def get_app(self):
    """Creates a web server which handles /service requests."""
    options.options.localdb = True
    options.options.fileobjstore = True
    options.options.localdb_dir = ''
    options.options.devbox = True
    options.options.domain = 'goviewfinder.com'
    options.options.short_domain = 'short.goviewfinder.com'

    # Init secrets with the unencrypted 'goviewfinder.com' domain.
    secrets.InitSecretsForTest()
    object_store.InitObjectStore(temporary=True)
    environ.ServerEnvironment.InitServerEnvironment()

    # Set up photo object store.
    obj_store = object_store.ObjectStore.GetInstance(object_store.ObjectStore.PHOTO)
    obj_store.SetUrlFmtString(self.get_url('/fileobjstore/photo/%s'))
    # Set up user logs object store.
    user_log_obj_store = object_store.ObjectStore.GetInstance(object_store.ObjectStore.USER_LOG)
    user_log_obj_store.SetUrlFmtString(self.get_url('/fileobjstore/user_log/%s'))
    # Set up user_zips object store.
    user_zips_obj_store = object_store.ObjectStore.GetInstance(object_store.ObjectStore.USER_ZIPS)
    user_zips_obj_store.SetUrlFmtString(self.get_url('/fileobjectstore/user_zips/%s'))

    settings = {
      'login_url': '/',
      'cookie_secret': secrets.GetSecret('cookie_secret'),
      'obj_store': obj_store,
      'server_version': ServiceTester.SERVER_VERSION,
      'google_client_id': secrets.GetSecret('google_client_id'),
      'google_client_secret': secrets.GetSecret('google_client_secret'),
      'google_client_mobile_id': secrets.GetSecret('google_client_mobile_id'),
      'google_client_mobile_secret': secrets.GetSecret('google_client_mobile_secret'),
      'facebook_api_key': secrets.GetSecret('facebook_api_key'),
      'facebook_secret': secrets.GetSecret('facebook_secret'),
      'template_path': ResourcesManager.Instance().template_path,
      'ui_modules': uimodules,
      'xsrf_cookies' : self._enable_xsrf,
      'static_path': ResourcesManager.Instance().static_path,
      }

    # Start with the production webapp handlers and add several for testing.
    webapp_handlers = deepcopy(server.WEBAPP_HANDLERS + server.ADMIN_HANDLERS)
    webapp_handlers.append((r'/fileobjstore/photo/(.*)',
                            file_object_store.FileObjectStoreHandler,
                            { 'storename': object_store.ObjectStore.PHOTO, 'contenttype': 'image/jpeg' }))
    webapp_handlers.append((r'/fileobjstore/user_log/(.*)',
                            file_object_store.FileObjectStoreHandler,
                            { 'storename': object_store.ObjectStore.USER_LOG, 'contenttype': 'text/plain' }))
    webapp_handlers.append((r'/fileobjstore/user_zips/(.*)',
                            file_object_store.FileObjectStoreHandler,
                            { 'storename': object_store.ObjectStore.USER_ZIPS, 'contenttype': 'application/zip' }))

    # Fake viewfinder handler - added explicitly because it is not part of WEBAPP_HANDLERS.
    webapp_handlers.append((r'/(link|login|register)/fakeviewfinder', auth_viewfinder.FakeAuthViewfinderHandler))

    application = web.Application(**settings)
    application.add_handlers(options.options.short_domain, server.SHORT_DOMAIN_HANDLERS)
    application.add_handlers('.*', webapp_handlers)
    return application
Exemple #2
0
    def javascript_files(self):
        jsfiles = ResourcesManager.Instance().GetAssetPaths('base_js')

        if environ.ServerEnvironment.IsDevBox():
            jsfiles.append('js/testutils.js')

        return jsfiles
Exemple #3
0
def _UploadWelcomePhotos(http_client, client, user, upload_request):
    """Uploads a set of photos that will be used in the new user welcome conversation. These
  photos are uploaded to the given user account. "upload_request" is in the UPLOAD_EPISODE_REQUEST
  format in json_schema.py, except:

    1. Activity, episode, and photo ids are added by this method.
    2. Each photo dict must contain an additional "name" field which gives the start of the
       filename of a jpg file in the backend/resources/welcome directory. Three files must
       exist there, in this format: <name>_full.jpg, <name>_med.jpg, <name>_tn.jpg.
  """
    obj_store = ObjectStore.GetInstance(ObjectStore.PHOTO)
    welcome_path = os.path.join(ResourcesManager.Instance().resources_path,
                                'welcome')

    # Set the ids of all activities, episodes, and photos in the welcome conversation.
    yield _SetWelcomeIds(user, upload_request)

    # Get copy and strip out names, which UploadEpisode chokes on.
    upload_request = deepcopy(upload_request)

    # Directly call the service API in order to upload the photo.
    upload_request_copy = deepcopy(upload_request)
    [ph_dict.pop('name') for ph_dict in upload_request_copy['photos']]
    upload_response = yield UploadEpisode(client, obj_store, user.user_id,
                                          user.webapp_dev_id,
                                          upload_request_copy)

    # Upload photo to blob store (in various formats).
    for request_ph_dict, response_ph_dict in zip(upload_request['photos'],
                                                 upload_response['photos']):
        for format in ('full', 'med', 'tn'):
            # Get the photo bits from disk.
            f = open(
                os.path.join(welcome_path,
                             '%s_%s.jpg' % (request_ph_dict['name'], format)),
                'r')
            image_data = f.read()
            f.close()

            photo_url = response_ph_dict[format + '_put_url']
            content_md5 = base64.b64encode(
                request_ph_dict[format + '_md5'].decode('hex'))
            headers = {
                'Content-Type': 'image/jpeg',
                'Content-MD5': content_md5
            }

            validate_cert = not options.options.fileobjstore
            response = yield gen.Task(http_client.fetch,
                                      photo_url,
                                      method='PUT',
                                      body=image_data,
                                      follow_redirects=False,
                                      validate_cert=validate_cert,
                                      headers=headers)
            if response.code != 200:
                raise Exception(
                    'Cannot upload photo "%s". HTTP error code %d. Is server running and accessible?'
                    % (request_ph_dict['photo_id'], response.code))
    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
Exemple #5
0
    def _CreateFormats():
        """Used to set up initial photos."""
        obj_store = ObjectStore.GetInstance(ObjectStore.PHOTO)
        client = DBClient.Instance()
        http_client = AsyncHTTPClient()

        for photo_id, name in [('pgAZn77bJ-Kc', 'beach_c4'),
                               ('pgAzpz7bJ-Mc', 'beach_a1'),
                               ('pgB-Fh7bJ-Mg', 'beach_a2'),
                               ('pgAzo67bJ-MV', 'beach_a3'),
                               ('pgB-pj7bJ-Mo', 'beach_a4'),
                               ('pgAvIa7bJ-MN', 'beach_b1'),
                               ('pgAuoQ7bJ-MF', 'beach_b2'),
                               ('pgAtwd7bJ-M7', 'beach_b3'),
                               ('pgAaOJ7bJ-Kw', 'beach_c1'),
                               ('pgA_vm7bJ-Ko', 'beach_c2'),
                               ('pgAZna7bJ-Kk', 'beach_c3'),
                               ('pgAW0x7bJ-KV', 'beach_d1'),
                               ('pgAUMm7bJ-KN', 'beach_d2'),
                               ('pfYwYR7bJ-KJ', 'party_1'),
                               ('pfYwTk7bJ-KF', 'party_2'),
                               ('pfYwSo7bJ-K7', 'party_3'),
                               ('pfYw0g7bJ-K-', 'party_4'),
                               ('pfYvoK7bJ-Jw', 'party_5'),
                               ('pfYvhI7bJ-Jo', 'party_6'),
                               ('prHKwa7bJ-N30', 'gone_fishing_1'),
                               ('prBUtl7bJ-Mw', 'gone_fishing_2'),
                               ('pfSb0S7bJ-Jk', 'street_art_1'),
                               ('pfSasJ7bJ-Jc', 'street_art_2')]:

            photo = yield Photo.Query(client, photo_id, None)
            photo_dict = photo._asdict()
            photo_dict['name'] = name
            del photo_dict['photo_id']
            del photo_dict['user_id']
            del photo_dict['_version']
            del photo_dict['episode_id']
            print json.dumps(photo_dict, indent=True)

            for suffix, format in [('.f', 'full'), ('.m', 'med'),
                                   ('.t', 'tn')]:
                url = obj_store.GenerateUrl('%s%s' % (photo_id, suffix))
                response = yield http_client.fetch(url, method='GET')

                welcome_path = os.path.join(
                    ResourcesManager.Instance().resources_path, 'welcome')
                f = open(
                    os.path.join(welcome_path, '%s_%s.jpg' % (name, format)),
                    'w')
                f.write(response.body)
                f.close()
Exemple #6
0
 def __init__(self, client, user_id, email):
     super(BuildArchiveOperation, self).__init__(client)
     self._user_id = user_id
     self._email = email
     self._notify_timestamp = self._op.timestamp
     self._photo_obj_store = ObjectStore.GetInstance(ObjectStore.PHOTO)
     self._user_zips_obj_store = ObjectStore.GetInstance(
         ObjectStore.USER_ZIPS)
     self._offboarding_assets_dir_path = ResourcesManager.Instance(
     ).GetOffboardingPath()
     self._temp_dir_path = os.path.join(
         ServerEnvironment.GetViewfinderTempDirPath(),
         BuildArchiveOperation._OFFBOARDING_DIR_NAME)
     self._zip_file_path = os.path.join(
         self._temp_dir_path, BuildArchiveOperation._ZIP_FILE_NAME)
     self._content_dir_path = os.path.join(
         self._temp_dir_path, BuildArchiveOperation._CONTENT_DIR_NAME)
     self._data_dir_path = os.path.join(self._content_dir_path,
                                        CONVO_FOLDER_NAME)
Exemple #7
0
 def css_files(self):
     return ResourcesManager.Instance().GetAssetPaths('admin_css')
Exemple #8
0
 def javascript_files(self):
     return ResourcesManager.Instance().GetAssetPaths('admin_js')
Exemple #9
0
 def javascript_files(self):
     """Currently includes all JS files used by view module."""
     resourceManager = ResourcesManager.Instance()
     return resourceManager.GetAssetPaths(
         'view_js') + resourceManager.GetAssetPaths('auth_js')
Exemple #10
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)
Exemple #11
0
def StartServer(serve_webapp=True, serve_static_web=True, serve_admin=True):
    """Initialize the datastore and operation manager with the viewfinder schema. This typically
  verifies the schema. If the schema does not yet exist, it is created.

  Defines settings dictionary and sets up main application with list of handlers.
  """
    client = db_client.DBClient.Instance()

    settings = {
        'gzip':
        True,
        'login_url':
        '/',
        'admin_login_url':
        '/admin/otp',
        'domain':
        options.options.domain,
        'server_version':
        options.options.server_version,
        'cookie_secret':
        secrets.GetSecret('cookie_secret'),
        'facebook_api_key':
        secrets.GetSecret('facebook_api_key'),
        'facebook_secret':
        secrets.GetSecret('facebook_secret'),
        'google_client_id':
        secrets.GetSecret('google_client_id'),
        'google_client_secret':
        secrets.GetSecret('google_client_secret'),
        'google_client_mobile_id':
        secrets.GetSecret('google_client_mobile_id'),
        'google_client_mobile_secret':
        secrets.GetSecret('google_client_mobile_secret'),
        'template_path':
        ResourcesManager.Instance().template_path,
        'ui_modules':
        uimodules,
        'xsrf_cookies':
        options.options.enable_xsrf,
        'debug':
        options.options.server_debug,
        'static_path':
        ResourcesManager.Instance().static_path,
    }

    if options.options.log_file_prefix:
        settings['logs_dir'] = os.path.dirname(options.options.log_file_prefix)

    # Configure metrics uploading.
    if options.options.upload_metrics:
        for interval in metric.METRIC_INTERVALS:
            metric.Metric.StartMetricUpload(client,
                                            metric.DEFAULT_CLUSTER_NAME,
                                            interval)

    # Setup application and SSL HTTP server.
    handlers = deepcopy(COMMON_HANDLERS)
    if serve_webapp:
        # Configure web application handlers.
        webapp_handlers = deepcopy(WEBAPP_HANDLERS)

        # Initialize the file object store if specified.
        obj_store = ObjectStore.GetInstance(ObjectStore.PHOTO)
        settings['obj_store'] = obj_store
        if options.options.fileobjstore:
            for store_name, content_type in ((ObjectStore.PHOTO,
                                              r'image/jpeg'),
                                             (ObjectStore.USER_LOG,
                                              r'text/plain'),
                                             (ObjectStore.USER_ZIPS,
                                              r'application/zip')):
                webapp_handlers.append(
                    (r'/fileobjstore/%s/(.*)' % store_name,
                     file_object_store.FileObjectStoreHandler, {
                         'storename': store_name,
                         'contenttype': content_type
                     }))

        if ServerEnvironment.IsDevBox():
            webapp_handlers.append((r'/(link|login|register)/fakeviewfinder',
                                    auth_viewfinder.FakeAuthViewfinderHandler))
            # Set the testing directories.
            if options.options.testing_path is not None:
                webapp_handlers.append(
                    (r'/testing/hook/(.*)', test_hook.TestHookHandler))
                webapp_handlers.append(
                    (r'/testing/static/(.*)', web.StaticFileHandler, {
                        'path': '%s' % options.options.testing_path
                    }))

        handlers.extend(webapp_handlers)

    if serve_static_web:
        # Configure static web handlers.
        static_web_handlers = deepcopy(STATIC_WEB_HANDLERS)
        handlers.extend(static_web_handlers)

    if serve_admin:
        # Configure and verify admin handlers.
        admin_handlers = deepcopy(ADMIN_HANDLERS)
        for path, handler in admin_handlers:
            if not issubclass(handler, basic_auth.BasicAuthHandler):
                raise TypeError('Administration handlers must '
                                'subclass BasicAuthHandler')
        handlers.extend(admin_handlers)

    # Catch-all handler for 404 pages.
    handlers.extend([(r'/.*', base.PageNotFoundHandler)])

    # Create application and separately add handlers for the short domain and the
    # regular domain.
    #
    # Note that, although the short-domain handlers are added after the initial construction
    # of the Application, those routes will take priority over the routes in the handlers
    # array.
    application = web.Application(handlers, **settings)
    application.add_handlers(re.escape(options.options.short_domain),
                             SHORT_DOMAIN_HANDLERS)

    # Start the HTTP server.
    http_server = httpserver.HTTPServer(
        application,
        xheaders=options.options.xheaders,
        ssl_options={
            'certfile': secrets.GetSecretFile('%s.crt' % settings['domain']),
            'keyfile': secrets.GetSecretFile('%s.key' % settings['domain']),
        } if options.options.ssl else None)
    with stack_context.NullContext():
        http_server.listen(options.options.port)

    # Setup redirect server for HTTP -> HTTPS.
    if options.options.ssl:
        http_settings = {
            'host': ServerEnvironment.GetHost(),
            'redirect_port': options.options.redirect_port,
            'xheaders': options.options.xheaders,
        }

        redirect_handlers = [
            (r'/(.*)', index.RedirectHandler),
        ]
        redirect_server = httpserver.HTTPServer(
            web.Application(redirect_handlers, **http_settings))
        with stack_context.NullContext():
            redirect_server.listen(options.options.insecure_port)

    # Ensure that system users have been created if running with a local db (needs server to be running).
    if options.options.localdb:
        yield CreateSystemUsers(client)

    # Run the server until it hits an exception or stop signal.
    yield gen.Task(lambda callback: None)
Exemple #12
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)