Initial import
This commit is contained in:
commit
c54898da7a
|
@ -0,0 +1,4 @@
|
||||||
|
venv
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
*.egg-info
|
|
@ -0,0 +1,13 @@
|
||||||
|
This is a simple file storage and permanent archive service.
|
||||||
|
It allows people to drop off files which are then assigned a uuid to link to.
|
||||||
|
|
||||||
|
=Install dependencies=
|
||||||
|
|
||||||
|
export VENV=/path/to/venv
|
||||||
|
python3 -m venv $VENV
|
||||||
|
$VENV/bin/pip install -e .
|
||||||
|
|
||||||
|
=Run the application stand-alone=
|
||||||
|
|
||||||
|
$VENV/bin/pserve settings.ini --reload
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import os
|
||||||
|
import shelve
|
||||||
|
|
||||||
|
from wsgiref.simple_server import make_server
|
||||||
|
from pyramid.config import Configurator
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Main entrance point that returns a wsgi application
|
||||||
|
"""
|
||||||
|
def main(global_config, **settings):
|
||||||
|
config = Configurator(settings=settings)
|
||||||
|
|
||||||
|
# Home view when visiting the app on /
|
||||||
|
config.add_route('home', '/')
|
||||||
|
|
||||||
|
# REST-API view behind /files
|
||||||
|
config.add_route('files', '/files')
|
||||||
|
config.add_route('files_uuid', '/files/{file_id}')
|
||||||
|
config.add_route('file_details', '/files/{file_id}/info')
|
||||||
|
config.add_route('file_uuid_key', '/files/{file_id}/{file_key}')
|
||||||
|
|
||||||
|
# Route for static files
|
||||||
|
config.add_static_view(name='static', path='filestorage:static')
|
||||||
|
|
||||||
|
# Add pyramid_jinja2 module to app config for web_views
|
||||||
|
config.include('pyramid_jinja2')
|
||||||
|
|
||||||
|
# Do a config scan to get all the views
|
||||||
|
config.scan()
|
||||||
|
|
||||||
|
# Make sure the infrastructure (folders, database) are set up
|
||||||
|
setup_infrastructure(config.registry.settings)
|
||||||
|
|
||||||
|
# Finally build and return the app
|
||||||
|
app = config.make_wsgi_app()
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Checkup routine to make sure folders and database are fine
|
||||||
|
"""
|
||||||
|
def setup_infrastructure(settings):
|
||||||
|
# Check temp folder
|
||||||
|
tmp_folder = settings['path_tmp']
|
||||||
|
if not os.path.exists(tmp_folder):
|
||||||
|
os.makedirs(tmp_folder)
|
||||||
|
if not os.access(tmp_folder, os.W_OK):
|
||||||
|
raise PermissionError('Unable to write to temporary folder: %s' % tmp_folder)
|
||||||
|
# Check file storage folder
|
||||||
|
path_file_storage = settings['path_file_storage']
|
||||||
|
if not os.path.exists(path_file_storage):
|
||||||
|
os.makedirs(path_file_storage)
|
||||||
|
if not os.access(path_file_storage, os.W_OK):
|
||||||
|
raise PermissionError('Unable to write to file storage folder: %s' % path_file_storage)
|
||||||
|
# Make sure shelve database works (and exists)
|
||||||
|
path_database = settings['path_database']
|
||||||
|
db = shelve.open(path_database, flag='c')
|
||||||
|
db.close()
|
||||||
|
pass
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import shelve
|
||||||
|
import shutil
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from pyramid.view import view_defaults
|
||||||
|
from pyramid.view import view_config
|
||||||
|
from pyramid.response import Response, FileResponse
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
This REST API provides a file resource to upload, update, download
|
||||||
|
and delete files.
|
||||||
|
"""
|
||||||
|
@view_defaults(route_name='files', renderer='json')
|
||||||
|
class RESTView(object):
|
||||||
|
def __init__(self, request):
|
||||||
|
self.request = request
|
||||||
|
self.path_db = request.registry.settings['path_database']
|
||||||
|
self.path_tmp = request.registry.settings['path_tmp']
|
||||||
|
self.path_file_storage = request.registry.settings['path_file_storage']
|
||||||
|
|
||||||
|
def db_fetch_file(self, file_id):
|
||||||
|
with shelve.open(self.path_db, flag='r') as db:
|
||||||
|
if file_id not in db:
|
||||||
|
raise LookupError('File with uuid %s does not exist!' % uuid)
|
||||||
|
else:
|
||||||
|
return db[file_id]
|
||||||
|
|
||||||
|
def db_add_file(self, file_id, data):
|
||||||
|
with shelve.open(self.path_db, flag='w') as db:
|
||||||
|
if file_id in db:
|
||||||
|
raise Error('File with uuid %s already exists!' % uuid)
|
||||||
|
else:
|
||||||
|
db[file_id] = data
|
||||||
|
|
||||||
|
def db_update_file(self, file_id, data):
|
||||||
|
with shelve.open(self.path_db, flag='w') as db:
|
||||||
|
if file_id not in db:
|
||||||
|
raise LookupError('File with uuid %s does not exist, cannot update!' % uuid)
|
||||||
|
else:
|
||||||
|
db[file_id] = data
|
||||||
|
|
||||||
|
def db_delete_file(self, file_id):
|
||||||
|
with shelve.open(self.path_db, flag='w') as db:
|
||||||
|
if file_id in db:
|
||||||
|
del db[file_id]
|
||||||
|
else:
|
||||||
|
raise LookupError('File with uuid %s does not exist, cannot delete!' % uuid)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Helper method to calculate hash of file
|
||||||
|
"""
|
||||||
|
def hash_file(self, filename, hasher):
|
||||||
|
BLOCKSIZE = 65536
|
||||||
|
with open(filename, 'rb') as infile:
|
||||||
|
buf = infile.read(BLOCKSIZE)
|
||||||
|
while len(buf) > 0:
|
||||||
|
hasher.update(buf)
|
||||||
|
buf = infile.read(BLOCKSIZE)
|
||||||
|
return hasher.hexdigest()
|
||||||
|
|
||||||
|
@view_config(request_method='PUT')
|
||||||
|
def put(self):
|
||||||
|
try:
|
||||||
|
# Create a fresh uuid for the new file
|
||||||
|
file_id = str(uuid.uuid4())
|
||||||
|
# Use PUT /files to create a new file
|
||||||
|
input_file = self.request.POST['file'].file
|
||||||
|
# Write the data into a temporary file
|
||||||
|
temp_file_path = os.path.join(self.path_tmp, '%s' % file_id) + '~'
|
||||||
|
input_file.seek(0)
|
||||||
|
with open(temp_file_path, 'wb') as output_file:
|
||||||
|
shutil.copyfileobj(input_file, output_file)
|
||||||
|
# Now that we know the file has been fully saved to disk move it into place.
|
||||||
|
target_file_path = os.path.join(self.path_file_storage, '%s' % file_id)
|
||||||
|
os.rename(temp_file_path, target_file_path)
|
||||||
|
# Calculate file hash
|
||||||
|
filehash_md5 = self.hash_file(target_file_path, hashlib.md5())
|
||||||
|
filehash_sha1 = self.hash_file(target_file_path, hashlib.sha1())
|
||||||
|
filehash_sha256 = self.hash_file(target_file_path, hashlib.sha256())
|
||||||
|
# Gather all data about the file
|
||||||
|
file_data = dict(uuid=file_id,name=self.request.POST['file'].filename, key=str(uuid.uuid4()), create_utc=str(datetime.datetime.utcnow()), sha256=filehash_sha256, sha1=filehash_sha1, md5=filehash_md5)
|
||||||
|
# Store file data in database
|
||||||
|
self.db_add_file(file_id, file_data)
|
||||||
|
# Hopefully everything worked out, so return the data from the file
|
||||||
|
return file_data
|
||||||
|
except:
|
||||||
|
return dict(success=False)
|
||||||
|
|
||||||
|
@view_config(request_method='GET', route_name='files_uuid')
|
||||||
|
def get(self):
|
||||||
|
# Use GET /files/<uuid> to retrieve the file
|
||||||
|
file_id = self.request.matchdict['file_id']
|
||||||
|
try:
|
||||||
|
file_data = self.db_fetch_file(file_id)
|
||||||
|
external_filename = file_data['name']
|
||||||
|
file_response = FileResponse(os.path.join(self.path_file_storage, '%s' % file_id))
|
||||||
|
file_response.content_disposition = 'attachment; filename=%s' % external_filename
|
||||||
|
return file_response
|
||||||
|
except:
|
||||||
|
return dict(success=False)
|
||||||
|
|
||||||
|
@view_config(request_method='GET', route_name='file_details')
|
||||||
|
def get_info(self):
|
||||||
|
# Use GET /files/<uuid>/info to get file details
|
||||||
|
file_id = self.request.matchdict['file_id']
|
||||||
|
try:
|
||||||
|
file_data = self.db_fetch_file(file_id)
|
||||||
|
del file_data['key'] # Remove the secret key only the creator is supposed to know
|
||||||
|
return file_data
|
||||||
|
except:
|
||||||
|
return dict(success=False)
|
||||||
|
|
||||||
|
@view_config(request_method='DELETE', route_name='file_uuid_key')
|
||||||
|
def delete(self):
|
||||||
|
# Use DELETE /files/<uuid>/<key> to delete file
|
||||||
|
file_id = self.request.matchdict['file_id']
|
||||||
|
file_key = self.request.matchdict['file_key']
|
||||||
|
try:
|
||||||
|
file_data = self.db_fetch_file(file_id)
|
||||||
|
except:
|
||||||
|
return dict(success=False)
|
||||||
|
if file_data['key'] == file_key:
|
||||||
|
# Delete file and db entry
|
||||||
|
file_path = os.path.join(self.path_file_storage, '%s' % file_id)
|
||||||
|
os.remove(file_path)
|
||||||
|
self.db_delete_file(file_id)
|
||||||
|
return dict(success=True)
|
||||||
|
else:
|
||||||
|
return dict(success=False)
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
body {
|
||||||
|
margin: 2em;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
# TODO: Implement the jQuery REST API client!
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,29 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Filestorage Service</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="{{ request.static_url('filestorage:static/app.css') }}"/>
|
||||||
|
<script type="text/javascript" src="{{ request.static_url('filestorage:static/jquery-3.1.1.min.js') }}"></script>
|
||||||
|
<script type="text/javascript" src="{{ request.static_url('filestorage:static/app.js') }}"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Welcome!</h1>
|
||||||
|
<p>
|
||||||
|
This site enables users to upload files for temporary or permanent access.
|
||||||
|
Since the PUT method of the API is used to upload files, please enable javascript.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Use this command to upload a file using curl:
|
||||||
|
<pre>curl {{ request.route_url('files') }} -X PUT -F "file=@/path/to/your/file"</pre>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Use this form to upload a file! (TODO: Implement with jQuery, properly display results)
|
||||||
|
<form action="/files" method="PUT" accept-charset="utf-8" enctype="multipart/form-data">
|
||||||
|
<label for="file">File</label>
|
||||||
|
<input id="file" name="file" type="file" value="" />
|
||||||
|
<input type="submit" value="Upload!" />
|
||||||
|
</form>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,44 @@
|
||||||
|
import cgi
|
||||||
|
|
||||||
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
from pyramid.response import Response
|
||||||
|
from pyramid.view import view_config
|
||||||
|
|
||||||
|
|
||||||
|
@view_config(route_name='home', renderer='templates/home.jinja2')
|
||||||
|
def home_view(request):
|
||||||
|
return dict()
|
||||||
|
# This is how params can be passed to the renderer
|
||||||
|
#return dict(name=request.matchdict['name'])
|
||||||
|
|
||||||
|
"""
|
||||||
|
# /howdy?name=alice which links to the next view
|
||||||
|
@view_config(route_name='hello')
|
||||||
|
def hello_view(request):
|
||||||
|
name = request.params.get('name', 'No Name')
|
||||||
|
body = '<p>Hi %s, this <a href="/goto">redirects</a></p>'
|
||||||
|
# cgi.escape to prevent Cross-Site Scripting (XSS) [CWE 79]
|
||||||
|
return Response(body % cgi.escape(name))
|
||||||
|
|
||||||
|
|
||||||
|
# /goto which issues HTTP redirect to the last view
|
||||||
|
@view_config(route_name='redirect')
|
||||||
|
def redirect_view(request):
|
||||||
|
return HTTPFound(location="/problem")
|
||||||
|
|
||||||
|
|
||||||
|
# /problem which causes a site error
|
||||||
|
@view_config(route_name='exception')
|
||||||
|
def exception_view(request):
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
# /get which shows a file by its uuid
|
||||||
|
@view_config(route_name='get_file')
|
||||||
|
def get_file_view(request):
|
||||||
|
body = 'Param: $(uuid)' % request.matchdict
|
||||||
|
return Response(body)
|
||||||
|
|
||||||
|
view_config(route_name='home', renderer='json')
|
||||||
|
def hello_json(request):
|
||||||
|
return [1, 2, 3]
|
||||||
|
"""
|
|
@ -0,0 +1,10 @@
|
||||||
|
[app:main]
|
||||||
|
use = egg:filestorage
|
||||||
|
path_tmp = /tmp
|
||||||
|
path_file_storage = /tmp/bucket
|
||||||
|
path_database = /tmp/filestorage.shelve
|
||||||
|
|
||||||
|
[server:main]
|
||||||
|
use = egg:pyramid#wsgiref
|
||||||
|
host = 127.0.0.1
|
||||||
|
port = 6543
|
Loading…
Reference in New Issue