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