383 lines
11 KiB
Python
383 lines
11 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import os,sys,time,stat
|
|
import json
|
|
from datetime import datetime
|
|
from flask import Flask, render_template, jsonify, current_app, \
|
|
redirect, url_for, request, g, session, send_file, send_from_directory
|
|
from werkzeug.utils import secure_filename
|
|
import hashlib
|
|
import zipfile
|
|
from revprox import ReverseProxied
|
|
|
|
app = Flask(__name__)
|
|
app.config.from_object(__name__)
|
|
# Read config from json !
|
|
config_values = json.load(open(os.getenv('FLEES_CONFIG'),'rt'))
|
|
app.config['UPLOAD_FOLDER'] = config_values['data_folder']
|
|
app.config['SHARES_FILE'] = config_values['shares_file']
|
|
app.config['ZIP_FOLDER'] = config_values['zip_folder']
|
|
app.config['MAX_ZIP_SIZE'] = config_values['max_zip_size'] # megabytes
|
|
app.config['DATE_FORMAT'] = config_values['date_format']
|
|
app.config['UID'] = config_values['uid']
|
|
app.config['GID'] = config_values['gid']
|
|
app.config['DEBUG'] = config_values['debug']
|
|
|
|
app.secret_key = config_values['app_secret_key']
|
|
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
|
|
|
@app.before_request
|
|
def before_request():
|
|
g.shares = json.load(open(app.config['SHARES_FILE'],'rt'))
|
|
|
|
@app.route("/")
|
|
def index():
|
|
public_shares = []
|
|
for share in g.shares:
|
|
public = get_or_none(share,'public')
|
|
expired = is_expired(share)
|
|
authenticated_share = get_share(share['name'])
|
|
password_set = False
|
|
if authenticated_share[0]:
|
|
password_set = authenticated_share[1]['authenticated'] == 'hash'
|
|
if not expired:
|
|
if public or password_set:
|
|
public_shares.append({
|
|
'name': share['name'],
|
|
'expire': get_or_none(share,'expire'),
|
|
'upload': get_or_none(share,'upload'),
|
|
'password_set': password_set
|
|
})
|
|
|
|
return render_template("index.html", entries=public_shares)
|
|
|
|
@app.route('/authenticate/<name>', methods=['GET','POST'])
|
|
def authenticate(name):
|
|
if request.method == 'GET':
|
|
return render_template('authenticate.html',name=name)
|
|
if request.method == 'POST':
|
|
user_password = request.form['password'].encode('utf-8')
|
|
session[name] = hashlib.sha1(user_password).hexdigest()
|
|
return redirect(url_for('list_view',name=name))
|
|
|
|
@app.route('/upload/<name>/<password>', methods=['POST'])
|
|
@app.route('/upload', methods=['POST'])
|
|
def upload(name = None, password = None):
|
|
if request.method == 'POST':
|
|
file = request.files['file']
|
|
if name == None:
|
|
name = request.form['name']
|
|
if password != None:
|
|
session[name] = password
|
|
(ok,share) = get_share(name)
|
|
if not ok:
|
|
return share
|
|
if not get_or_none(share,'upload') == True:
|
|
return "Upload not allowed",400
|
|
if file:
|
|
filename = os.path.join(
|
|
share['path'],
|
|
secure_filename(
|
|
file.filename
|
|
)
|
|
)
|
|
if get_or_none(share, 'overwrite') == False:
|
|
if os.path.exists(filename):
|
|
return "Overwrite forbidden", 403
|
|
file.save(filename)
|
|
set_rights(filename)
|
|
if 'from_gui' in request.form:
|
|
if request.form['from_gui'] == "true":
|
|
return redirect(url_for('list_view',name=name))
|
|
return "File uploaded\n", 200
|
|
else:
|
|
return "Use the 'file' variable to upload",400
|
|
|
|
@app.route('/send/<name>', methods=['GET'])
|
|
def send(name):
|
|
(ok,share) = get_share(name)
|
|
if not ok:
|
|
return share
|
|
return render_template('send.html',name=name)
|
|
|
|
@app.route('/list/<name>/<password>', methods=['GET'])
|
|
@app.route('/list/<name>', methods=['GET'])
|
|
def list_view(name, password = None):
|
|
if password != None:
|
|
session[name] = password
|
|
(ok,share) = get_share(name)
|
|
if not ok:
|
|
return share
|
|
files = []
|
|
for file in sorted(os.listdir(share['path'])):
|
|
fp = os.path.join(share['path'],file)
|
|
if os.path.isdir(fp):
|
|
continue
|
|
status = file_stat(fp)
|
|
status.update({
|
|
'token': get_direct_token(share, file)
|
|
})
|
|
files.append(status)
|
|
# direct share links not allowed if password isnt set
|
|
allow_direct = get_or_none(share,'direct_links') if get_or_none(share,'pass_hash') else False
|
|
return render_template(
|
|
"list.html",
|
|
name = share['name'],
|
|
entries = files,
|
|
password = get_or_none(share,'pass_hash'),
|
|
public = get_or_none(share,'public'),
|
|
upload = get_or_none(share,'upload'),
|
|
overwrite = get_or_none(share,'overwrite'),
|
|
direct = allow_direct,
|
|
expire = get_or_none(share,'expire'),
|
|
description = get_or_none(share,'description',"")
|
|
)
|
|
|
|
@app.route('/logout/<name>', methods=['GET'])
|
|
def logout(name, password = None):
|
|
if name in session:
|
|
del session[name]
|
|
return render_template(
|
|
"logout.html",
|
|
name = name
|
|
)
|
|
|
|
@app.route('/direct/<name>/<password>/<filename>', methods=['GET'])
|
|
def download_direct(name,password,filename):
|
|
if password != None:
|
|
session[name] = password
|
|
(ok,share) = get_share(name, require_auth = False)
|
|
if not ok:
|
|
return share
|
|
allow_direct = get_or_none(share,'direct_links')
|
|
if allow_direct != True:
|
|
return 'Direct download not allowed', 403
|
|
token = get_direct_token(share, filename)
|
|
if token == None:
|
|
return 'Cannot generate token', 400
|
|
if password != token:
|
|
return 'Incorrect token', 403
|
|
file_path = os.path.join(share['path'], filename)
|
|
if not os.path.exists(file_path):
|
|
return 'no such file', 404
|
|
return send_from_directory(directory=share['path'], filename=filename)
|
|
|
|
|
|
@app.route('/download/<name>/<password>/<filename>', methods=['GET'])
|
|
@app.route('/download/<name>/<filename>', methods=['GET'])
|
|
def download_file(name,filename,password = None):
|
|
if password != None:
|
|
session[name] = password
|
|
(ok,share) = get_share(name)
|
|
if not ok:
|
|
return share
|
|
file_path = os.path.join(share['path'], filename)
|
|
if not os.path.exists(file_path):
|
|
return 'no such file', 404
|
|
return send_from_directory(directory=share['path'], filename=filename)
|
|
|
|
|
|
@app.route('/zip/<name>/<password>', methods=['GET'])
|
|
@app.route('/zip/<name>', methods=['GET'])
|
|
def download_zip(name,password = None):
|
|
if password != None:
|
|
session[name] = password
|
|
(ok,share) = get_share(name)
|
|
if not ok:
|
|
return share
|
|
folder_size = get_folder_size(share['path'])
|
|
if folder_size/(1024*1024) > app.config['MAX_ZIP_SIZE']:
|
|
return "Maximum ZIP size exceeded", 400
|
|
zip_clean()
|
|
zip_path = zip_share(share)
|
|
return send_file(
|
|
zip_path,
|
|
as_attachment = True,
|
|
attachment_filename = name + ".zip"
|
|
)
|
|
|
|
@app.route('/script/upload/<name>/<password>', methods=['GET'])
|
|
def script_upload(name = None, password = None):
|
|
session[name] = password
|
|
(ok,share) = get_share(name)
|
|
if not ok:
|
|
return share
|
|
if not get_or_none(share,'upload') == True:
|
|
return "Upload not allowed",400
|
|
return """#!/bin/bash
|
|
test -f "$1" || {
|
|
echo "Add files to upload as argument"
|
|
exit 1
|
|
}
|
|
CAT=$( which cat )
|
|
which pv &> /dev/null && CAT=$( which pv )
|
|
|
|
for file_name in "$@"; do
|
|
base_name=$( basename "$file_name" )
|
|
test -f "$file_name" || {
|
|
echo "'$file_name' not a file"
|
|
continue
|
|
}
|
|
|
|
$CAT "$file_name" | curl -F "file=@-;filename=${base_name}" %supload/%s/%s
|
|
done
|
|
"""%(
|
|
request.url_root,
|
|
name,
|
|
password
|
|
)
|
|
|
|
|
|
def file_stat(filename):
|
|
s = os.stat(filename)
|
|
return {
|
|
'size': file_size_MB(s.st_size),
|
|
'mtime': file_date_human(s.st_mtime),
|
|
'name': os.path.basename(filename)
|
|
}
|
|
|
|
|
|
def file_size_human(num):
|
|
for x in [' B','KB','MB','GB','TB']:
|
|
if num < 1024.0:
|
|
if x==' B':
|
|
return "%d %s" % (num, x)
|
|
return "%3.1f %s" % (num, x)
|
|
num /= 1024.0
|
|
|
|
def file_size_MB(num):
|
|
return "%0.2f"%(num/(1024*1024),)
|
|
|
|
|
|
def file_date_human(num):
|
|
return datetime.fromtimestamp(
|
|
num
|
|
).strftime(app.config['DATE_FORMAT'])
|
|
|
|
|
|
def get_direct_token(share, filename):
|
|
if not 'pass_hash' in share:
|
|
return None
|
|
return hashlib.sha1(
|
|
share['pass_hash'].encode('utf-8') + filename.encode('utf-8')
|
|
).hexdigest()
|
|
|
|
def get_folder_size(path):
|
|
|
|
total_size = 0
|
|
for filename in os.listdir(path):
|
|
fp = os.path.join(path, filename)
|
|
if os.path.isdir(fp):
|
|
continue
|
|
total_size += os.path.getsize(
|
|
fp
|
|
)
|
|
return total_size
|
|
|
|
|
|
def get_or_none(d,key,none = None):
|
|
if key in d:
|
|
return d[key]
|
|
else:
|
|
return none
|
|
|
|
|
|
def get_share(name, require_auth = True):
|
|
share = [x for x in g.shares if x['name'] == name]
|
|
if len(share) < 1:
|
|
return (False,redirect(url_for('authenticate',name=name)))
|
|
share = share[0]
|
|
if is_expired(share):
|
|
return (False, 'Share has expired')
|
|
authenticated = "no-pass"
|
|
if require_auth:
|
|
if 'pass_hash' in share:
|
|
authenticated = False
|
|
if name in session:
|
|
if session[name] == share['pass_hash']:
|
|
authenticated = "hash"
|
|
|
|
if not authenticated:
|
|
return (False,redirect(url_for('authenticate',name=name)))
|
|
if not 'path' in share:
|
|
return (False,'no path defined')
|
|
share.update({
|
|
"path": os.path.join(
|
|
app.config['UPLOAD_FOLDER'],
|
|
share['path']
|
|
),
|
|
"authenticated": authenticated
|
|
})
|
|
if not os.path.exists(share['path']):
|
|
os.makedirs(share['path'])
|
|
set_rights(share['path'])
|
|
return (True,share)
|
|
|
|
def is_expired(share):
|
|
expires = get_or_none(share, 'expire')
|
|
if expires:
|
|
if datetime.now() > datetime.strptime(expires, app.config['DATE_FORMAT']):
|
|
return True
|
|
return False
|
|
|
|
def print_debug(s):
|
|
if app.config['DEBUG']:
|
|
sys.stderr.write(str(s)+"\n")
|
|
sys.stderr.flush()
|
|
|
|
def set_rights(path):
|
|
os.chown(path, app.config['UID'], app.config['GID'])
|
|
st = os.stat(path)
|
|
if app.config['UID'] > 0:
|
|
os.chmod(path, st.st_mode | stat.S_IRUSR | stat.S_IWUSR)
|
|
if app.config['GID'] > 0:
|
|
os.chmod(path, st.st_mode | stat.S_IRGRP | stat.S_IWGRP)
|
|
|
|
|
|
|
|
def zip_share(share):
|
|
|
|
if not os.path.exists(app.config['ZIP_FOLDER']):
|
|
os.makedirs(app.config['ZIP_FOLDER'])
|
|
set_rights(app.config['ZIP_FOLDER'])
|
|
|
|
zip_path = os.path.join(
|
|
app.config['ZIP_FOLDER'],
|
|
"%s-%d.zip"%(
|
|
share['name'],
|
|
time.time()
|
|
)
|
|
)
|
|
zf = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
|
|
for file in sorted(os.listdir(share['path'])):
|
|
fp = os.path.join(share['path'], file)
|
|
if os.path.isdir(fp):
|
|
continue
|
|
print_debug(fp)
|
|
zf.write(
|
|
fp,
|
|
arcname = os.path.join(share['name'],file)
|
|
)
|
|
zf.close()
|
|
set_rights(zip_path)
|
|
return zip_path
|
|
|
|
def zip_clean():
|
|
""" delete zip files older than 1 hour """
|
|
if not os.path.exists(app.config['ZIP_FOLDER']):
|
|
return
|
|
for file in os.listdir(app.config['ZIP_FOLDER']):
|
|
if not file.endswith("zip"):
|
|
continue
|
|
mtime = os.stat(
|
|
os.path.join(app.config['ZIP_FOLDER'],file)
|
|
).st_mtime
|
|
if mtime + 3600 < time.time():
|
|
os.remove(os.path.join(app.config['ZIP_FOLDER'],file))
|
|
|
|
if __name__ == "__main__":
|
|
app.run(debug=True)
|
|
|
|
|