Ejemplo n.º 1
0
def Start(callback):
    client = db_client.DBClient.Instance()

    job = Job(client, 'generate_pdf_reports')
    if options.options.require_lock:
        got_lock = yield gen.Task(job.AcquireLock)
        if got_lock == False:
            logging.warning('Failed to acquire job lock: exiting.')
            callback()
            return

    if options.options.send_email:
        # When running on devbox, this prompts for the passphrase. Skip if not sending email.
        EmailManager.SetInstance(SendGridEmailManager())
    else:
        EmailManager.SetInstance(LoggingEmailManager())

    # We never call job.Start() since we don't want a summary status written to the DB, just the lock.
    try:
        yield gen.Task(RunOnce, client)
    except:
        logging.error(traceback.format_exc())
    finally:
        yield gen.Task(job.ReleaseLock)

    callback()
def Start(callback):
    assert sys.platform == 'darwin', 'This script can only run on OSX. You are using %s' % sys.platform
    """Grab a lock on job:client_logs_analysis and call RunOnce."""
    client = db_client.DBClient.Instance()
    job = Job(client, 'client_logs_analysis')

    if options.options.send_email:
        # When running on devbox, this prompts for the passphrase. Skip if not sending email.
        EmailManager.SetInstance(SendGridEmailManager())
    else:
        EmailManager.SetInstance(LoggingEmailManager())

    if options.options.require_lock:
        got_lock = yield gen.Task(job.AcquireLock)
        if got_lock == False:
            logging.warning('Failed to acquire job lock: exiting.')
            callback()
            return

    # We never call job.Start() since we don't want a summary status written to the DB, just the lock.
    try:
        yield gen.Task(RunOnce, client)
    except:
        logging.error(traceback.format_exc())
    finally:
        yield gen.Task(job.ReleaseLock)

    callback()
Ejemplo n.º 3
0
def _StartWWW(run_callback, scan_ops):
    """Starts services necessary for operating in the Viewfinder WWW server environment. Invokes
  'run_callback' asynchronously.
  """
    client = db_client.DBClient.Instance()

    # Log emails and texts to the console in local mode.
    if options.options.local_services:
        EmailManager.SetInstance(LoggingEmailManager())
        SMSManager.SetInstance(LoggingSMSManager())
    else:
        EmailManager.SetInstance(SendGridEmailManager())
        SMSManager.SetInstance(TwilioSMSManager())

    # Set URL for local fileobjstores.
    if options.options.fileobjstore:
        # Import server for ssl and port options.
        from viewfinder.backend.www import server
        url_fmt_string = '%s://%s:%d/fileobjstore/' % (
            'https' if options.options.ssl else 'http',
            ServerEnvironment.GetHost(), options.options.port)
        url_fmt_string += '%s/%%s'
        for store_name in (ObjectStore.PHOTO, ObjectStore.USER_LOG,
                           ObjectStore.USER_ZIPS):
            ObjectStore.GetInstance(store_name).SetUrlFmtString(
                url_fmt_string % store_name)

    OpManager.SetInstance(
        OpManager(op_map=DB_OPERATION_MAP, client=client, scan_ops=scan_ops))

    apns_feedback_handler = Device.FeedbackHandler(client)
    APNS.SetInstance(
        'dev', APNS(environment='dev', feedback_handler=apns_feedback_handler))
    APNS.SetInstance(
        'ent', APNS(environment='ent', feedback_handler=apns_feedback_handler))
    APNS.SetInstance(
        'prod', APNS(environment='prod',
                     feedback_handler=apns_feedback_handler))
    http_client = AsyncHTTPClient()
    ITunesStoreClient.SetInstance(
        'dev', ITunesStoreClient(environment='dev', http_client=http_client))
    ITunesStoreClient.SetInstance(
        'prod', ITunesStoreClient(environment='prod', http_client=http_client))

    # Ensure that system users are loaded.
    yield LoadSystemUsers(client)

    yield gen.Task(run_callback)
Ejemplo n.º 4
0
def _Start(callback):
    """Grab a lock on job:server_log_metrics and call RunOnce. If we get a return value, write it to the job summary."""
    if options.options.send_email:
        # When running on devbox, this prompts for the passphrase. Skip if not sending email.
        EmailManager.SetInstance(SendGridEmailManager())
    else:
        EmailManager.SetInstance(LoggingEmailManager())

    client = db_client.DBClient.Instance()
    job = Job(client, 'server_log_metrics')

    if options.options.require_lock:
        got_lock = yield gen.Task(job.AcquireLock)
        if got_lock == False:
            logging.warning('Failed to acquire job lock: exiting.')
            callback()
            return

    is_full_run = all([
        options.options.compute_user_requests,
        options.options.compute_registration_delay,
        options.options.compute_app_versions
    ])

    result = None
    job.Start()
    try:
        result = yield gen.Task(RunOnce, client, job)
    except:
        # Failure: log run summary with trace.
        typ, val, tb = sys.exc_info()
        msg = ''.join(traceback.format_exception(typ, val, tb))
        logging.info('Registering failed run with message: %s' % msg)
        yield gen.Task(job.RegisterRun, Job.STATUS_FAILURE, failure_msg=msg)
    else:
        if result is not None and not options.options.dry_run and is_full_run:
            # Successful full run with data processed and not in dry-run mode: write run summary.
            stats = DotDict()
            stats['last_day'] = result
            logging.info('Registering successful run with stats: %r' % stats)
            yield gen.Task(job.RegisterRun, Job.STATUS_SUCCESS, stats=stats)
    finally:
        yield gen.Task(job.ReleaseLock)

    callback()
Ejemplo n.º 5
0
def Run(callback):
    assert options.email_template, '--email_template must be set'

    EmailManager.SetInstance(SendGridEmailManager())
    SMSManager.SetInstance(TwilioSMSManager())

    # Load the email template.
    f = open(options.email_template, "r")
    email_template = Template(f.read())
    f.close()

    # Load the SMS template.
    if options.sms_template:
        f = open(options.sms_template, "r")
        sms_template = Template(f.read())
        f.close()

    sms_warning = False
    client = DBClient.Instance()
    last_user_id = None
    count = 0
    while True:
        users = yield GetRegisteredUsers(client, last_user_id)
        if not users:
            break

        count += len(users)
        print 'Scanned %d users...' % count
        for user in users:
            last_user_id = user.user_id
            if options.min_user_id != -1 and user.user_id < options.min_user_id:
                continue

            if options.max_user_id != -1 and user.user_id > options.max_user_id:
                continue

            # Only send to users which allow marketing communication to them.
            if options.honor_allow_marketing:
                settings = yield gen.Task(AccountSettings.QueryByUser, client,
                                          user.user_id, None)
                if not settings.AllowMarketing():
                    continue

            if user.email:
                yield SendEmailToUser(email_template, user)
            elif user.phone:
                if not options.sms_template:
                    if not sms_warning:
                        print 'WARNING: no SMS template specified and phone-only accounts encountered; skipping...'
                        sms_warning = True
                else:
                    yield SendSMSToUser(sms_template, user)

    callback()
Ejemplo n.º 6
0
def SendEmailToUser(template, user):
    assert user.email is not None, user

    unsubscribe_cookie = User.CreateUnsubscribeCookie(
        user.user_id, AccountSettings.MARKETING)
    unsubscribe_url = 'https://%s/unsubscribe?%s' % (
        options.domain, urlencode(dict(cookie=unsubscribe_cookie)))

    # Create arguments for the email template.
    fmt_args = {
        'first_name': user.given_name,
        'unsubscribe_url': unsubscribe_url
    }

    # Create arguments for the email.
    args = {
        'from': EmailManager.Instance().GetInfoAddress(),
        'fromname': 'Viewfinder',
        'to': user.email,
        'subject': options.email_subject
    }
    util.SetIfNotNone(args, 'toname', user.name)

    args['html'] = template.generate(is_html=True, **fmt_args)
    args['text'] = template.generate(is_html=False, **fmt_args)

    print 'Sending marketing email to %s (%s) (#%d)' % (user.email, user.name,
                                                        user.user_id)

    if options.test_mode:
        global _is_first_email
        if _is_first_email:
            print args['html']
            _is_first_email = False
    else:
        # Remove extra whitespace in the HTML (seems to help it avoid Gmail spam filter).
        args['html'] = escape.squeeze(args['html'])

        yield gen.Task(EmailManager.Instance().SendEmail,
                       description='marketing email',
                       **args)
def SendEmail(title, text, callback):
    args = {
        'from': '*****@*****.**',
        'fromname': 'Symbolicator',
        'to': options.options.email,
        'subject': title,
        'text': text
    }
    yield gen.Task(EmailManager.Instance().SendEmail,
                   description=title,
                   **args)
    callback()
Ejemplo n.º 8
0
def _SendEmail(from_name, title, text, callback):
    args = {
        'from': '*****@*****.**',
        'fromname': from_name,
        'to': options.options.email,
        'subject': title,
        'text': text
    }
    yield gen.Task(EmailManager.Instance().SendEmail,
                   description=title,
                   **args)
    callback()
Ejemplo n.º 9
0
def SendEmail(title, text, callback):
    args = {
        'from': '*****@*****.**',
        'fromname': 'Viewfinder reports',
        'to': options.options.email,
        'subject': title,
        'text': text
    }
    yield gen.Task(EmailManager.Instance().SendEmail,
                   description=title,
                   **args)
    callback()
Ejemplo n.º 10
0
    def _GetAuthEmail(cls, client, action, use_short_token, user_name,
                      identity, short_url):
        """Returns a dict of parameters that will be passed to EmailManager.SendEmail in order to
    email an access token to a user who is verifying his/her account.
    """
        action_info = VerifyIdBaseHandler.ACTION_MAP[action]
        identity_type, identity_value = Identity.SplitKey(identity.key)

        # Create arguments for the email.
        args = {
            'from': EmailManager.Instance().GetInfoAddress(),
            'fromname': 'Viewfinder',
            'to': identity_value
        }
        util.SetIfNotNone(args, 'toname', user_name)

        # Create arguments for the email template.
        fmt_args = {
            'user_name':
            user_name or identity_value,
            'user_email':
            identity_value,
            'url':
            'https://%s/%s%s' % (ServerEnvironment.GetHost(),
                                 short_url.group_id, short_url.random_key),
            'title':
            action_info.title,
            'use_short_token':
            use_short_token,
            'access_token':
            identity.access_token
        }

        # The email html format is designed to meet these requirements:
        #   1. It must be viewable on even the most primitive email html viewer. Avoid fancy CSS.
        #   2. It cannot contain any images. Some email systems (like Gmail) do not show images by default.
        #   3. It must be short and look good on an IPhone 4S screen. The action button should be visible
        #      without any scrolling necessary.
        resources_mgr = ResourcesManager.Instance()
        if use_short_token:
            args['subject'] = 'Viewfinder Code: %s' % identity.access_token
        else:
            args['subject'] = action_info.title
        args['html'] = resources_mgr.GenerateTemplate(
            action_info.email_template, is_html=True, **fmt_args)
        args['text'] = resources_mgr.GenerateTemplate(
            action_info.email_template, is_html=False, **fmt_args)

        # Remove extra whitespace in the HTML (seems to help it avoid Gmail spam filter).
        args['html'] = escape.squeeze(args['html'])

        return args
Ejemplo n.º 11
0
    def SendFollowerAlert(cls, client, user_id, badge, viewpoint, follower,
                          settings, activity):
        """Sends an APNS and/or email alert to the given follower according to his alert settings."""
        # Only send add_followers alert to users who were added.
        if activity.name == 'add_followers':
            args_dict = json.loads(activity.json)
            if user_id not in args_dict['follower_ids']:
                return

        if follower.IsMuted():
            # User has muted this viewpoint, so don't send any alerts.
            return

        if settings.push_alerts is not None and settings.push_alerts != AccountSettings.PUSH_NONE:
            alert_text = yield AlertManager._FormatAlertText(
                client, viewpoint, activity)
            if alert_text is not None:
                # Only alert with sound if this is the first unread activity for the conversation.
                if follower.viewed_seq + 1 >= viewpoint.update_seq:
                    sound = PushNotification.DEFAULT_SOUND
                else:
                    sound = None

                viewpoint_id = viewpoint.viewpoint_id if viewpoint is not None else None
                yield AlertManager._SendDeviceAlert(client,
                                                    user_id,
                                                    viewpoint_id,
                                                    badge,
                                                    alert_text,
                                                    sound=sound)

        if settings.email_alerts is not None and settings.email_alerts != AccountSettings.EMAIL_NONE:
            alert_email_args = yield AlertManager._FormatAlertEmail(
                client, user_id, viewpoint, activity)
            if alert_email_args is not None:
                # Possible failure of email alert should not propagate.
                try:
                    yield gen.Task(EmailManager.Instance().SendEmail,
                                   description=activity.name,
                                   **alert_email_args)
                except:
                    logging.exception('failed to send alert email user %d',
                                      user_id)

        if settings.sms_alerts is not None and settings.sms_alerts != AccountSettings.SMS_NONE:
            alert_sms_args = yield AlertManager._FormatAlertSMS(
                client, user_id, viewpoint, activity)
            if alert_sms_args is not None:
                # Don't keep sending SMS alerts if the user hasn't clicked previous links in a while.
                sms_count = settings.sms_count or 0
                if sms_count == AlertManager._SMS_ALERT_LIMIT:
                    # Send SMS alert telling user that we won't send any more until they click link.
                    text = alert_sms_args['text']
                    alert_sms_args['text'] = 'You haven\'t viewed photos shared to you on Viewfinder. Do you want to ' \
                                             'continue receiving these links? If yes, click: %s' % text[text.rfind('https://'):]

                if sms_count <= AlertManager._SMS_ALERT_LIMIT:
                    # Possible failure of SMS alert should not propagate.
                    try:
                        yield gen.Task(SMSManager.Instance().SendSMS,
                                       description=activity.name,
                                       **alert_sms_args)
                    except:
                        logging.exception(
                            'failed to send alert SMS message user %d',
                            user_id)

                # Increment the SMS alert count.
                settings.sms_count = sms_count + 1
                yield gen.Task(settings.Update, client)
Ejemplo n.º 12
0
    def _FormatConversationEmail(cls, client, recipient_id, viewpoint,
                                 activity):
        """Constructs an email which alerts the recipient that they have access to a new
    conversation, either due to a share_new operation, or to an add_followers operation.
    The email includes a clickable link to the conversation on the web site.
    """
        from viewfinder.backend.db.identity import Identity
        from viewfinder.backend.db.photo import Photo
        from viewfinder.backend.db.user import User

        # Get email address of recipient.
        recipient_user = yield gen.Task(User.Query, client, recipient_id, None)
        if recipient_user.email is None:
            # No email address associated with user, so can't send email.
            raise gen.Return(None)

        identity_key = 'Email:%s' % recipient_user.email

        # Create ShortURL that sets prospective user cookie and then redirects to the conversation.
        viewpoint_url = yield AlertManager._CreateViewpointURL(
            client, recipient_user, identity_key, viewpoint)

        sharer = yield gen.Task(User.Query, client, activity.user_id, None)
        sharer_name = AlertManager._GetNameFromUser(sharer,
                                                    prefer_given_name=False)

        # Create the cover photo ShortURL by appending a "next" query parameter to the viewpoint ShortURL.
        cover_photo_url = None
        cover_photo_height = None
        cover_photo_width = None
        if viewpoint.cover_photo != None:
            next_url = '/episodes/%s/photos/%s.f' % (
                viewpoint.cover_photo['episode_id'],
                viewpoint.cover_photo['photo_id'])
            cover_photo_url = "%s?%s" % (viewpoint_url,
                                         urlencode(dict(next=next_url)))

            photo = yield gen.Task(Photo.Query, client,
                                   viewpoint.cover_photo['photo_id'], None)

            if photo.aspect_ratio < 1:
                cover_photo_height = AlertManager._MAX_COVER_PHOTO_DIM
                cover_photo_width = int(AlertManager._MAX_COVER_PHOTO_DIM *
                                        photo.aspect_ratio)
            else:
                cover_photo_width = AlertManager._MAX_COVER_PHOTO_DIM
                cover_photo_height = int(AlertManager._MAX_COVER_PHOTO_DIM /
                                         photo.aspect_ratio)

        email_args = {
            'from': EmailManager.Instance().GetInfoAddress(),
            'to': recipient_user.email,
            'subject': '%s added you to a conversation' % sharer_name
        }
        util.SetIfNotEmpty(email_args, 'toname', recipient_user.name)
        if sharer_name:
            email_args['fromname'] = '%s via Viewfinder' % sharer_name

        # Create the unsubscribe URL.
        unsubscribe_cookie = User.CreateUnsubscribeCookie(
            recipient_id, AccountSettings.EMAIL_ALERTS)
        unsubscribe_url = 'https://%s/unsubscribe?%s' % (
            options.options.domain, urlencode(dict(cookie=unsubscribe_cookie)))

        # Set viewpoint title.
        viewpoint_title = viewpoint.title if viewpoint is not None else None

        fmt_args = {
            'cover_photo_url': cover_photo_url,
            'cover_photo_height': cover_photo_height,
            'cover_photo_width': cover_photo_width,
            'viewpoint_url': viewpoint_url,
            'unsubscribe_url': unsubscribe_url,
            'sharer_name': sharer_name,
            'viewpoint_title': viewpoint_title,
            'toname': recipient_user.name
        }

        resources_mgr = ResourcesManager.Instance()

        email_args['html'] = escape.squeeze(
            resources_mgr.GenerateTemplate('alert_conv_base.email',
                                           is_html=True,
                                           **fmt_args))
        email_args['text'] = resources_mgr.GenerateTemplate(
            'alert_conv_base.email', is_html=False, **fmt_args)

        raise gen.Return(email_args)
Ejemplo n.º 13
0
    def SendVerifyIdMessage(cls, client, action, use_short_token,
                            is_mobile_app, identity_key, user_id, user_name,
                            **kwargs):
        """Sends a verification email or SMS message to the given identity. This message may
    directly contain an access code (e.g. if an SMS is sent), or it may contain a ShortURL
    link to a page which reveals the access code (e.g. if email was triggered by the mobile
    app). Or it may contain a link to a page which confirms the user's password and redirects
    them to the web site (e.g. if email was triggered by the web site).
    """
        # Ensure that identity exists.
        identity = yield gen.Task(Identity.Query,
                                  client,
                                  identity_key,
                                  None,
                                  must_exist=False)
        if identity is None:
            identity = Identity.CreateFromKeywords(key=identity_key)
            yield gen.Task(identity.Update, client)

        identity_type, identity_value = Identity.SplitKey(identity.key)
        message_type = 'emails' if identity_type == 'Email' else 'messages'

        # Throttle the rate at which email/SMS messages can be sent to this identity. The updated
        # count will be saved by CreateAccessTokenURL.
        auth_throttle = identity.auth_throttle or {}

        per_min_dict, is_throttled = util.ThrottleRate(
            auth_throttle.get('per_min',
                              None), VerifyIdBaseHandler._MAX_MESSAGES_PER_MIN,
            constants.SECONDS_PER_MINUTE)
        if is_throttled:
            # Bug 485: Silently do not send the email if throttled. We don't want to give user error
            #          if they exit out of confirm code screen, then re-create account, etc.
            return

        per_day_dict, is_throttled = util.ThrottleRate(
            auth_throttle.get('per_day',
                              None), VerifyIdBaseHandler._MAX_MESSAGES_PER_DAY,
            constants.SECONDS_PER_DAY)
        if is_throttled:
            raise InvalidRequestError(TOO_MANY_MESSAGES_DAY,
                                      message_type=message_type,
                                      identity_value=Identity.GetDescription(
                                          identity.key))

        identity.auth_throttle = {
            'per_min': per_min_dict,
            'per_day': per_day_dict
        }

        # Create a ShortURL link that will supply the access token to the user when clicked.
        # Use a URL path like "idm/*" for the mobile app, and "idw/*" for the web.
        encoded_user_id = base64hex.B64HexEncode(
            util.EncodeVarLengthNumber(user_id), padding=False)
        group_id = '%s/%s' % ('idm' if is_mobile_app else 'idw',
                              encoded_user_id)
        short_url = yield gen.Task(identity.CreateAccessTokenURL,
                                   client,
                                   group_id,
                                   use_short_token=use_short_token,
                                   action=action,
                                   identity_key=identity.key,
                                   user_name=user_name,
                                   **kwargs)

        # Send email/SMS in order to verify that the user controls the identity.
        if identity_type == 'Email':
            args = VerifyIdBaseHandler._GetAuthEmail(client, action,
                                                     use_short_token,
                                                     user_name, identity,
                                                     short_url)
            yield gen.Task(EmailManager.Instance().SendEmail,
                           description=action,
                           **args)
        else:
            args = VerifyIdBaseHandler._GetAccessTokenSms(identity)
            yield gen.Task(SMSManager.Instance().SendSMS,
                           description=action,
                           **args)

        # In dev servers, display a popup with the generated code (OS X 10.9-only).
        if (options.options.localdb and platform.system() == 'Darwin'
                and platform.mac_ver()[0] == '10.9'):
            subprocess.call([
                'osascript', '-e',
                'display notification "%s" with title "Viewfinder"' %
                identity.access_token
            ])
Ejemplo n.º 14
0
    def _BuildArchive(self):
        """Drive overall archive process as outlined in class header comment."""

        logging.info('building archive for user: %d' % self._user_id)

        # Prepare temporary destination folder (delete existing.  We'll always start from scratch).
        self._ResetArchiveDir()

        # Copy in base assets and javascript which will drive browser experience of content for users.
        proc = process.Subprocess([
            'cp', '-R',
            os.path.join(self._offboarding_assets_dir_path, 'web_code'),
            self._content_dir_path
        ])
        code = yield gen.Task(proc.set_exit_callback)
        if code != 0:
            logging.error('Error copying offboarding assets: %d' % code)
            raise IOError()

        # Top level iteration is over viewpoints.
        # For each viewpoint,
        #    iterate over activities and collect photos/episodes as needed.
        #    Build various 'tables' in json format:
        #        Activity, Comment, Episode, Photo, ...
        #
        viewpoints_dict = yield _QueryFollowedForArchive(
            self._client, self._user_id)
        viewpoint_ids = [
            viewpoint['viewpoint_id']
            for viewpoint in viewpoints_dict['viewpoints']
        ]
        followers_dict = yield _QueryViewpointsForArchive(self._client,
                                                          self._user_id,
                                                          viewpoint_ids,
                                                          get_followers=True)
        for viewpoint, followers in zip(viewpoints_dict['viewpoints'],
                                        followers_dict['viewpoints']):
            viewpoint['followers'] = followers
        # Query user info for all users referenced by any of the viewpoints.
        users_to_query = list({
            f['follower_id']
            for vp in followers_dict['viewpoints'] for f in vp['followers']
        })
        users_dict = yield _QueryUsersForArchive(self._client, self._user_id,
                                                 users_to_query)
        top_level_metadata_dict = dict(viewpoints_dict.items() +
                                       users_dict.items())

        # Write the top level metadata to the root of the archive.
        # TODO(mike): Consider moving this IO to thread pool to avoid blocking on main thread.
        with open(os.path.join(self._content_dir_path, 'viewpoints.jsn'),
                  mode='wb') as f:
            # Need to set metadata as variable for JS code.
            f.write("viewfinder.jsonp_data =")
            json.dump(top_level_metadata_dict, f)

        # Now, process each viewpoint.
        for vp_dict in top_level_metadata_dict['viewpoints']:
            if Follower.REMOVED not in vp_dict['labels']:
                yield self._ProcessViewpoint(vp_dict)

        # Now, generate user specific view file: index.html.
        # This is the file that the user will open to launch the web client view of their data.
        recipient_user = yield gen.Task(User.Query, self._client,
                                        self._user_id, None)
        user_info = {
            'user_id': recipient_user.user_id,
            'name': recipient_user.name,
            'email': recipient_user.email,
            'phone': recipient_user.phone,
            'default_viewpoint_id': recipient_user.private_vp_id
        }
        view_local = ResourcesManager().Instance().GenerateTemplate(
            'view_local.html', user_info=user_info, viewpoint_id=None)
        with open(os.path.join(self._content_dir_path, 'index.html'),
                  mode='wb') as f:
            f.write(view_local)

        with open(os.path.join(self._content_dir_path, 'README.txt'),
                  mode='wb') as f:
            f.write(
                "This Viewfinder archive contains both a readable local HTML file "
                +
                "and backup folders including all photos included in those conversations.\n"
            )

        # Exec zip command relative to the parent of content dir so that paths in zip are relative to that.
        proc = process.Subprocess([
            'zip', '-r', BuildArchiveOperation._ZIP_FILE_NAME,
            BuildArchiveOperation._CONTENT_DIR_NAME
        ],
                                  cwd=self._temp_dir_path)
        code = yield gen.Task(proc.set_exit_callback)
        if code != 0:
            logging.error('Error creating offboarding zip file: %d' % code)
            raise IOError()

        # Key is: "{user_id}/{timestamp}_{random}/Viewfinder.zip"
        # timestamp is utc unix timestamp.
        s3_key = '%d/%d_%d/Viewfinder.zip' % (
            self._user_id,
            calendar.timegm(datetime.datetime.utcnow().utctimetuple()),
            int(random.random() * 1000000))

        if options.options.fileobjstore:
            # Next, upload this to S3 (really fileobjstore in this case).
            with open(self._zip_file_path, mode='rb') as f:
                s3_data = f.read()
            yield gen.Task(self._user_zips_obj_store.Put, s3_key, s3_data)
        else:
            # Running against AWS S3, so use awscli to upload zip file into S3.
            s3_path = 's3://' + ObjectStore.USER_ZIPS_BUCKET + '/' + s3_key

            # Use awscli to copy file into S3.
            proc = process.Subprocess(
                [
                    'aws', 's3', 'cp', self._zip_file_path, s3_path,
                    '--region', 'us-east-1'
                ],
                stdout=process.Subprocess.STREAM,
                stderr=process.Subprocess.STREAM,
                env={
                    'AWS_ACCESS_KEY_ID': GetSecret('aws_access_key_id'),
                    'AWS_SECRET_ACCESS_KEY': GetSecret('aws_secret_access_key')
                })

            result, error, code = yield [
                gen.Task(proc.stdout.read_until_close),
                gen.Task(proc.stderr.read_until_close),
                gen.Task(proc.set_exit_callback)
            ]

            if code != 0:
                logging.error("%d = 'aws s3 cp %s %s': %s" %
                              (code, self._zip_file_path, s3_path, error))
                if result and len(result) > 0:
                    logging.info("aws result: %s" % result)
                raise IOError()

        # Generate signed URL to S3 for given user zip.  Only allow link to live for 3 days.
        s3_url = self._user_zips_obj_store.GenerateUrl(
            s3_key,
            cache_control='private,max-age=%d' %
            self._S3_ZIP_FILE_ACCESS_EXPIRATION,
            expires_in=3 * self._S3_ZIP_FILE_ACCESS_EXPIRATION)
        logging.info('user zip uploaded: %s' % s3_url)

        # Finally, send the user an email with the link to download the zip files just uploaded to s3.
        email_args = {
            'from': EmailManager.Instance().GetInfoAddress(),
            'to': self._email,
            'subject': 'Your Viewfinder archive download is ready'
        }

        fmt_args = {
            'archive_url': s3_url,
            'hello_name': recipient_user.given_name or recipient_user.name
        }
        email_args['text'] = ResourcesManager.Instance().GenerateTemplate(
            'user_zip.email', is_html=False, **fmt_args)
        yield gen.Task(EmailManager.Instance().SendEmail,
                       description='user archive zip',
                       **email_args)