new version allows downloads from subfolders
This commit is contained in:
91
code/app.py
91
code/app.py
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os,sys,time,stat
|
||||
@@ -8,13 +7,13 @@ from flask import Flask, render_template, jsonify, current_app, Response, \
|
||||
redirect, url_for, request, g, session, send_file, send_from_directory
|
||||
from werkzeug.utils import secure_filename
|
||||
import zipfile
|
||||
import urllib
|
||||
from multiprocessing import Process
|
||||
from revprox import ReverseProxied
|
||||
from utils.utils import *
|
||||
from utils.crypt import *
|
||||
|
||||
__FLEES_VERSION__ = "20180302.0"
|
||||
|
||||
__FLEES_VERSION__ = "20180310.0"
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(__name__)
|
||||
# Read config from json !
|
||||
@@ -42,6 +41,7 @@ if 'notifier' in config_values:
|
||||
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'))
|
||||
@@ -49,6 +49,7 @@ def before_request():
|
||||
g.site_name = app.config['SITE_NAME']
|
||||
g.max_zip_size = app.config['MAX_ZIP_SIZE']
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
public_shares = []
|
||||
@@ -71,6 +72,7 @@ def index():
|
||||
|
||||
return render_template("index.html", entries=public_shares)
|
||||
|
||||
|
||||
@app.route('/authenticate/<name>', methods=['GET','POST'])
|
||||
def authenticate(name):
|
||||
if request.method == 'GET':
|
||||
@@ -80,6 +82,7 @@ def authenticate(name):
|
||||
session[name] = password_hash(user_password, app.secret_key)
|
||||
return redirect(url_for('list_view',name=name))
|
||||
|
||||
|
||||
@app.route('/upload/<name>/<token>', methods=['POST'])
|
||||
@app.route('/upload', methods=['POST'])
|
||||
def upload(name = None, token = None):
|
||||
@@ -158,9 +161,6 @@ def upload_join_splitted(name, token):
|
||||
except:
|
||||
return "Joining failed\n", 400
|
||||
return "Joining started\n", 200
|
||||
#~ return Response(joiner(target_name, parts), mimetype="text/plain", content_type="text/event-stream")
|
||||
|
||||
#~ return "%d parts joined"%(len(parts),), 200
|
||||
|
||||
|
||||
@app.route('/send/<name>', methods=['GET'])
|
||||
@@ -170,22 +170,19 @@ def send(name):
|
||||
return share
|
||||
return render_template('send.html',name=name)
|
||||
|
||||
|
||||
@app.route('/files/<name>/<token>', methods=['GET'])
|
||||
def list_files(name, token):
|
||||
(ok,share) = get_share(name, token = token)
|
||||
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
|
||||
if file.startswith("."):
|
||||
continue
|
||||
files.append(urllib.parse.quote_plus(file))
|
||||
for file in iter_folder_files(share['path']):
|
||||
files.append(path2url(file))
|
||||
files.append("")
|
||||
return "\n".join(files), 200
|
||||
|
||||
|
||||
@app.route('/list/<name>/<token>', methods=['GET'])
|
||||
@app.route('/list/<name>', methods=['GET'])
|
||||
def list_view(name, token = None):
|
||||
@@ -197,15 +194,13 @@ def list_view(name, token = None):
|
||||
return redirect(url_for('list_view',name=name))
|
||||
|
||||
files = []
|
||||
for file in sorted(os.listdir(share['path'])):
|
||||
for file in iter_folder_files(share['path']):
|
||||
fp = os.path.join(share['path'],file)
|
||||
if os.path.isdir(fp):
|
||||
continue
|
||||
if file.startswith("."):
|
||||
continue
|
||||
status = file_stat(fp)
|
||||
status.update({
|
||||
'token': get_direct_token(share, file)
|
||||
'token': get_direct_token(share, file),
|
||||
'name': file,
|
||||
'url': path2url(file)
|
||||
})
|
||||
files.append(status)
|
||||
# direct share links not allowed if password isnt set
|
||||
@@ -227,6 +222,7 @@ def list_view(name, token = None):
|
||||
description = get_or_none('description', share, "")
|
||||
)
|
||||
|
||||
|
||||
@app.route('/logout/<name>', methods=['GET'])
|
||||
def logout(name):
|
||||
if name in session:
|
||||
@@ -236,7 +232,8 @@ def logout(name):
|
||||
name = name
|
||||
)
|
||||
|
||||
@app.route('/direct/<name>/<token>/<filename>', methods=['GET'])
|
||||
|
||||
@app.route('/direct/<name>/<token>/<path:filename>', methods=['GET'])
|
||||
def download_direct(name,token,filename):
|
||||
(ok,share) = get_share(name, require_auth = False)
|
||||
if not ok:
|
||||
@@ -244,6 +241,8 @@ def download_direct(name,token,filename):
|
||||
allow_direct = get_or_none('direct_links', share)
|
||||
if allow_direct != True:
|
||||
return 'Direct download not allowed', 403
|
||||
if not is_path_safe(filename):
|
||||
return 'Incorrect relative path'+filename, 403
|
||||
file_token = get_direct_token(share, filename)
|
||||
if file_token == None:
|
||||
return 'Cannot generate token', 400
|
||||
@@ -251,7 +250,7 @@ def download_direct(name,token,filename):
|
||||
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 'No such file', 404
|
||||
notify({
|
||||
"recipient": get_or_none('recipient', share),
|
||||
"share": name,
|
||||
@@ -261,22 +260,14 @@ def download_direct(name,token,filename):
|
||||
return send_from_directory(directory=share['path'], filename=filename)
|
||||
|
||||
|
||||
@app.route('/download/<name>/<token>/<filename>', methods=['GET'])
|
||||
@app.route('/download/<name>/<filename>', methods=['GET'])
|
||||
def download_file(name, filename, token = None):
|
||||
(ok,share) = get_share(name, token = token)
|
||||
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
|
||||
notify({
|
||||
"recipient": get_or_none('recipient', share),
|
||||
"share": name,
|
||||
"filename": file_path,
|
||||
"operation": "download"
|
||||
})
|
||||
return send_from_directory(directory=share['path'], filename=filename)
|
||||
@app.route('/download/gui/<name>/<path:filename>', methods=['GET'])
|
||||
def download_gui(name, filename):
|
||||
return download_file(name, filename, token = None)
|
||||
|
||||
|
||||
@app.route('/download/<name>/<token>/<path:filename>', methods=['GET'])
|
||||
def download_token(name, filename, token):
|
||||
return download_file(name, filename, token = token)
|
||||
|
||||
|
||||
@app.route('/zip/<name>/<token>', methods=['GET'])
|
||||
@@ -302,6 +293,7 @@ def download_zip(name, token = None):
|
||||
attachment_filename = name + ".zip"
|
||||
)
|
||||
|
||||
|
||||
@app.route('/script/upload/<name>/<token>', methods=['GET'])
|
||||
def script_upload(name = None, token = None):
|
||||
(ok,share) = get_share(name, token = token)
|
||||
@@ -518,8 +510,8 @@ done
|
||||
token
|
||||
)
|
||||
|
||||
class uploadJoiner:
|
||||
|
||||
class uploadJoiner:
|
||||
def __init__(self, target_name, parts):
|
||||
self.target_name = target_name
|
||||
self.parts = parts
|
||||
@@ -528,6 +520,7 @@ class uploadJoiner:
|
||||
p.daemon = True
|
||||
p.start()
|
||||
|
||||
|
||||
def run(self):
|
||||
with open(self.target_name,'wb') as writer:
|
||||
for part in self.parts:
|
||||
@@ -539,6 +532,24 @@ class uploadJoiner:
|
||||
os.remove(part)
|
||||
|
||||
|
||||
def download_file(name, filename, token = None):
|
||||
(ok,share) = get_share(name, token = token)
|
||||
if not ok:
|
||||
return share
|
||||
if not is_path_safe(filename):
|
||||
return "Incorrect path", 403
|
||||
file_path = os.path.join(share['path'], filename)
|
||||
if not os.path.exists(file_path):
|
||||
return 'No such file, '+file_path, 404
|
||||
notify({
|
||||
"recipient": get_or_none('recipient', share),
|
||||
"share": name,
|
||||
"filename": file_path,
|
||||
"operation": "download"
|
||||
})
|
||||
return send_from_directory(directory=share['path'], filename=filename)
|
||||
|
||||
|
||||
def file_versionize(filename):
|
||||
""" Move file to versioned with integer """
|
||||
stats = file_stat(filename)
|
||||
@@ -635,7 +646,6 @@ def set_rights(path):
|
||||
|
||||
|
||||
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'])
|
||||
@@ -648,7 +658,7 @@ def zip_share(share):
|
||||
)
|
||||
)
|
||||
zf = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
|
||||
for file in sorted(os.listdir(share['path'])):
|
||||
for file in iter_folder_files(share['path']):
|
||||
fp = os.path.join(share['path'], file)
|
||||
if os.path.isdir(fp):
|
||||
continue
|
||||
@@ -677,6 +687,7 @@ def zip_clean():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
zip_clean()
|
||||
app.run(debug=True)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from datetime import datetime
|
||||
from utils.utils import *
|
||||
from utils.crypt import *
|
||||
|
||||
|
||||
def get_root_path(opts):
|
||||
root_folder = os.path.dirname(
|
||||
os.path.dirname(
|
||||
@@ -57,6 +58,7 @@ def list_folders(shares,config):
|
||||
for path, folders, files in os.walk(data_folder):
|
||||
full_path = os.path.join(data_folder, path)
|
||||
share_name = None
|
||||
parent_is_share = False
|
||||
for share in shares:
|
||||
share_path = os.path.join(data_folder, share['path'])
|
||||
if not os.path.exists(share_path):
|
||||
@@ -64,6 +66,13 @@ def list_folders(shares,config):
|
||||
if os.path.samefile(full_path, share_path):
|
||||
share_name = share['name']
|
||||
break
|
||||
parents = full_path.split(os.sep)
|
||||
for p in range(len(parents)):
|
||||
test_path = os.sep+os.sep.join(parents[1:(p+2)])
|
||||
if os.path.samefile(test_path, share_path):
|
||||
parent_is_share = True
|
||||
if parent_is_share:
|
||||
continue
|
||||
if share_name == None:
|
||||
# skip folder if it's not a share, and not a leaf
|
||||
if len(folders) > 0:
|
||||
@@ -83,6 +92,7 @@ def list_folders(shares,config):
|
||||
))
|
||||
print(tabulate(table, headers = "firstrow"))
|
||||
|
||||
|
||||
def add_share(shares, config, opts):
|
||||
|
||||
# Make name and path safe:
|
||||
@@ -214,7 +224,6 @@ def modify_share(shares, config, opts):
|
||||
if not token in share['tokens']:
|
||||
share['tokens'].append(token)
|
||||
|
||||
|
||||
if opts.expire:
|
||||
if opts.expire == "":
|
||||
# REMOVE EXPIRATION
|
||||
@@ -306,7 +315,6 @@ def show_share(shares, config, opts):
|
||||
print_share(share, config)
|
||||
|
||||
|
||||
|
||||
def print_rest_api(shares, config, opts):
|
||||
if 'public_url' not in config:
|
||||
print("Set public_url variable in your config.json")
|
||||
@@ -412,16 +420,12 @@ def print_rest_api_download(config, share, token):
|
||||
if not os.path.exists(share_path):
|
||||
print("no files")
|
||||
sys.exit(0)
|
||||
for filename in sorted(os.listdir(share_path)):
|
||||
if os.path.isdir(os.path.join(share_path,filename)):
|
||||
continue
|
||||
if filename.startswith("."):
|
||||
continue
|
||||
for filename in iter_folder_files(share_path):
|
||||
print("%s/download/%s/%s/%s"%(
|
||||
config['public_url'],
|
||||
share['name'],
|
||||
token,
|
||||
filename
|
||||
path2url(filename)
|
||||
))
|
||||
print("or \n\n# curl -s %s/script/download/%s/%s | bash /dev/stdin [-f]"%(
|
||||
config['public_url'],
|
||||
@@ -444,16 +448,12 @@ def print_rest_api_direct(config, share, token):
|
||||
if not os.path.exists(share_path):
|
||||
print("no files")
|
||||
sys.exit(0)
|
||||
for filename in sorted(os.listdir(share_path)):
|
||||
if os.path.isdir(os.path.join(share_path,filename)):
|
||||
continue
|
||||
if filename.startswith("."):
|
||||
continue
|
||||
for filename in iter_folder_files(share_path):
|
||||
print("%s/direct/%s/%s/%s"%(
|
||||
config['public_url'],
|
||||
share['name'],
|
||||
get_direct_token(share,filename),
|
||||
filename
|
||||
path2url(filename)
|
||||
))
|
||||
print("or \n\n# curl -s %s/script/direct/%s/%s | bash /dev/stdin [-f]"%(
|
||||
config['public_url'],
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
{% if direct %}
|
||||
<a href="{{ url_for('download_direct', name = name, token = entry.token, filename = entry.name ) }}" title="Direct share link" class=direct>❖</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('download_file', name = name, filename = entry.name) }}">{{ entry.name }}</a>
|
||||
<a href="{{ url_for('download_gui', name = name, filename = entry.url) }}">{{ entry.name }}</a>
|
||||
<td class=td_right>{{ entry.size|safe }}
|
||||
<td>{{ entry.mtime|safe }}
|
||||
</tr>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from flask import current_app as app
|
||||
|
||||
try:
|
||||
from urllib.request import pathname2url
|
||||
except ImportError:
|
||||
from urllib import pathname2url
|
||||
|
||||
def file_date_human(num):
|
||||
return datetime.fromtimestamp(
|
||||
@@ -47,12 +50,50 @@ def get_or_none(key,d,none = None):
|
||||
else:
|
||||
return none
|
||||
|
||||
|
||||
def is_path_safe(path):
|
||||
if path.startswith("."):
|
||||
return False
|
||||
if "/." in path:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def iter_folder_files(path, recursive = True):
|
||||
if recursive:
|
||||
for dirpath, dirnames, filenames in os.walk(path, topdown = False):
|
||||
relative_path = os.path.relpath(dirpath,path)
|
||||
dirnames.sort()
|
||||
if "/." in relative_path:
|
||||
continue
|
||||
if relative_path == ".":
|
||||
relative_path = ""
|
||||
for f in sorted(filenames):
|
||||
if f.startswith("."):
|
||||
continue
|
||||
fp = os.path.join(relative_path, f)
|
||||
yield fp
|
||||
else:
|
||||
for file in sorted(path):
|
||||
fp = os.path.join(path,file)
|
||||
if os.path.isdir(fp):
|
||||
continue
|
||||
if file.startswith("."):
|
||||
continue
|
||||
yield fp
|
||||
|
||||
|
||||
def path2url(path):
|
||||
return pathname2url(path)
|
||||
|
||||
def safe_name(s):
|
||||
return safe_string(s, "-_")
|
||||
|
||||
|
||||
def safe_path(s):
|
||||
return safe_string(s, "-_/")
|
||||
|
||||
|
||||
def safe_string(s, valid):
|
||||
""" return a safe string, replace non alnum characters with _ . all characters in valid are considered valid. """
|
||||
return "".join([c if c.isalnum() or c in valid else "_" for c in s])
|
||||
|
||||
Reference in New Issue
Block a user