@gen.coroutine
def UploadEpisode(client, obj_store, user_id, device_id, request):
  """Creates metadata for new photos and returns URLs where the client
  should upload the photo images. Creates a new episode to group the
  photos or adds the photos to an existing episode.
  """
  def _GenerateUploadUrl(obj_store, ph_dict, suffix, md5_attr):
    """Create an S3 URL that is the target of a photo upload. "suffix" is
    appended to the end of the photo id in order to distinguish different
    photo sizes. The value of the "md5_attr" attribute is converted to a
    base-64 encoded value for the HTTP Content-MD5 header. Using this
    attribute ensures that the user uploads a photo that exactly matches
    the MD5, and also enables detection of any bit corruption on the wire.
    """
    md5_hex = ph_dict[md5_attr]
    try:
      # Convert MD5 encoded as a hex string to base-64 string.
      content_md5 = base64.b64encode(md5_hex.decode('hex'))
    except TypeError:
      raise InvalidRequestError('md5 value "%s" is invalid; it must be in ascii hex format' % md5_hex)

    return obj_store.GenerateUploadUrl('%s%s' % (ph_dict['photo_id'], suffix),
                                       content_type=ph_dict.get('content_type', 'image/jpeg'),
                                       content_md5=content_md5)

  @gen.coroutine
  def _VerifyAttributeUpdate(photo, suffix, attr_name, new_value):
    """Checks whether the specified photo metadata attribute is about
    to be modified. Don't allow the change if the photo image data has
    already been uploaded to S3. This check prevents situations where
    photo MD5 values are updated after the initial upload, which could
    allow a user to overwrite an existing photo in order to bypass the
    7-day unshare rule. But it still allows cases where a new MD5 was
    generated by the client as part of a rebuild.
    """
    existing_value = getattr(photo, attr_name)
    if new_value != existing_value:
      # Existing attribute value does not match, so only allow it to be updated if the image data
      # has not yet been uploaded.
      etag = yield gen.Task(Photo.IsImageUploaded, obj_store, photo.photo_id, suffix)
      if etag is not None:
        raise PermissionError('cannot overwrite existing photo "%s" with a new photo' %
                              (photo.photo_id + suffix))

  @gen.coroutine
  def _VerifyPhoto(ph_dict):
    """Verify the photo's id. Override any MD5 values that have changed
    on the client as long as the photo image data hasn't yet been uploaded.
    """
    yield Photo.VerifyPhotoId(client, user_id, device_id, ph_dict['photo_id'])
    photo = yield gen.Task(Photo.Query, client, ph_dict['photo_id'], None, must_exist=False)

    if photo is not None:
      # Photo must be owned by calling user, or there's a security breach.
      assert photo.user_id == user_id, (photo, user_id)

      yield [gen.Task(_VerifyAttributeUpdate, photo, '.t', 'tn_md5', ph_dict['tn_md5']),
             gen.Task(_VerifyAttributeUpdate, photo, '.m', 'med_md5', ph_dict['med_md5']),
             gen.Task(_VerifyAttributeUpdate, photo, '.f', 'full_md5', ph_dict['full_md5']),
             gen.Task(_VerifyAttributeUpdate, photo, '.o', 'orig_md5', ph_dict['orig_md5'])]

  yield Activity.VerifyActivityId(client, user_id, device_id, request['activity']['activity_id'])

  request['user_id'] = user_id

  episode_id = request['episode']['episode_id']
  yield Episode.VerifyEpisodeId(client, user_id, device_id, episode_id)

  # In parallel, verify each photo id and update any attributes that can be overridden.
  yield [gen.Task(_VerifyPhoto, ph_dict) for ph_dict in request['photos']]

  yield gen.Task(Operation.CreateAndExecute,
                 client,
                 user_id,
                 device_id,
                 'UploadEpisodeOperation.Execute',
                 request)

  logging.info('UPLOAD EPISODE: user: %d, device: %d, episode: %s, %d photos' %
               (user_id, device_id, episode_id, len(request['photos'])))

  response = {'episode_id': episode_id,
              'photos': [{'photo_id': ph_dict['photo_id'],
                          'tn_put_url': _GenerateUploadUrl(obj_store, ph_dict, '.t', 'tn_md5'),
                          'med_put_url': _GenerateUploadUrl(obj_store, ph_dict, '.m', 'med_md5'),
                          'full_put_url': _GenerateUploadUrl(obj_store, ph_dict, '.f', 'full_md5'),
                          'orig_put_url': _GenerateUploadUrl(obj_store, ph_dict, '.o', 'orig_md5')}
                         for ph_dict in request['photos']]}

  raise gen.Return(response)
dataset/ETHPy150Open viewfinderco/viewfinder/backend/www/service.py/UploadEpisode/original