Repository: fossasia/open-event-server

Issue:
Implement a better storage schema
As a result of discussion at #1050, we found out that there were 2 issues with current implementation.
1. guessable urls - URLs right now follow a pattern and so are prone to scripts. 
2. low SEO - The filename of resource on server is something like `audio`, `video` etc. These don't describe the media well and thus have less SEO.

Solutions - 

1 - 

> https://github.com/fossasia/open-event-orga-server/issues/1050#issuecomment-231822943
> What we can do is use the current method of setting a KEY for an object
> eg. events/1/sessions/1/audio and then encrypt the KEY using app.secret_key as the key.
> Then add the encrypted string to the KEY. 
> So events/1/sessions/1/audio becomes events/1/sessions/1/audio/<ENCRYPTED> where ENCRYPTED is encrypted form of events/1/sessions/1/audio .
> (ENCRYPTED can be base64 encoded to make sure it is url friendly)
> As app.secret_key is supposed to be secret so this will be safe from scrapers.
> Also it won't have the disadvantage of having problems with keeping only distinct data on the server. (as ENCRYPTED will be same for same KEY)

2 - 
Store the original filename of the media file after the url. eg > 
`events/1/sessions/1/audio/<ENCRYPTED>/<filename>` . As far as atomicity of data is concerned, there is a method in boto (s3 lib) to delete all files inside a key. So we can delete everything inside `events/1/sessions/1/audio/<ENCRYPTED>/` at once easily. 



Discussion:

Resolving patch:
diff --git a/app/helpers/storage.py b/app/helpers/storage.py
--- a/app/helpers/storage.py
+++ b/app/helpers/storage.py
@@ -1,8 +1,10 @@
 import os
-from shutil import copyfile
-
+from base64 import b64encode
+from shutil import copyfile, rmtree
+from flask.ext.scrypt import generate_password_hash
 from boto.s3.connection import S3Connection
 from boto.s3.key import Key
+from werkzeug.utils import secure_filename
 
 from app.settings import get_settings
 
@@ -100,9 +102,15 @@ def upload_local(file, key, **kwargs):
     """
     Uploads file locally. Base dir - static/media/
     """
-    __, ext = os.path.splitext(file.filename)
-    file_path = 'static/media/' + key + ext
+    filename = secure_filename(file.filename)
+    file_path = 'static/media/' + key + '/' + generate_hash(key) + '/' + filename
     dir_path = file_path.rsplit('/', 1)[0]
+    # delete current
+    try:
+        rmtree(dir_path)
+    except OSError:
+        pass
+    # create dirs
     if not os.path.isdir(dir_path):
         os.makedirs(dir_path)
     file.save(file_path)
@@ -117,17 +125,20 @@ def upload_to_aws(bucket_name, aws_key, aws_secret, file, key, acl='public-read'
     conn = S3Connection(aws_key, aws_secret)
     bucket = conn.get_bucket(bucket_name)
     k = Key(bucket)
-    # generate key using key + extension
-    __, ext = os.path.splitext(file.filename)  # includes dot
-    k.key = key
-    key_name = key.rsplit('/')[-1]
+    # generate key
+    filename = secure_filename(file.filename)
+    key_dir = key + '/' + generate_hash(key) + '/'
+    k.key = key_dir + filename
+    # delete old data
+    for item in bucket.list(prefix='/' + key_dir):
+        item.delete()
     # set object settings
     file_data = file.read()
     size = len(file_data)
     sent = k.set_contents_from_string(
         file_data,
         headers={
-            'Content-Disposition': 'attachment; filename=%s%s' % (key_name, ext)
+            'Content-Disposition': 'attachment; filename=%s' % filename
         }
     )
     k.set_acl(acl)
@@ -135,3 +146,15 @@ def upload_to_aws(bucket_name, aws_key, aws_secret, file, key, acl='public-read'
     if sent == size:
         return s3_url + k.key
     return False
+
+
+# ########
+# HELPERS
+# ########
+
+def generate_hash(key):
+    """
+    Generate hash for key
+    """
+    phash = generate_password_hash(key, get_settings()['secret'])
+    return b64encode(phash)[:10]  # limit len to 10, is sufficient


Test patch: