new file versioning policy and manager. be sure to add version_folder in your conf
This commit is contained in:
45
code/app.py
45
code/app.py
@@ -14,7 +14,7 @@ from utils.utils import *
|
||||
from utils.crypt import *
|
||||
|
||||
|
||||
__FLEES_VERSION__ = "20180720.0"
|
||||
__FLEES_VERSION__ = "20180721.0"
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(__name__)
|
||||
# Read config from json !
|
||||
@@ -29,6 +29,7 @@ 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.config['VERSION_FOLDER'] = config_values['version_folder']
|
||||
if 'notifier' in config_values:
|
||||
if len(config_values['notifier']) > 0:
|
||||
notifier_config = config_values['notifier'].split(":")
|
||||
@@ -313,7 +314,7 @@ def file_list(name, token):
|
||||
if not ok:
|
||||
return share
|
||||
files = []
|
||||
for file in iter_folder_files(share['path']):
|
||||
for file in iter_folder_files(share['path'], version_folder = app.config['VERSION_FOLDER']):
|
||||
files.append(path2url(file))
|
||||
files.append("")
|
||||
return "\n".join(files), 200
|
||||
@@ -325,7 +326,7 @@ def file_details(name, token):
|
||||
if not ok:
|
||||
return share
|
||||
files = []
|
||||
for file in iter_folder_files(share['path']):
|
||||
for file in iter_folder_files(share['path'], version_folder = app.config['VERSION_FOLDER']):
|
||||
status = file_stat(share['path'],file)
|
||||
files.append(status)
|
||||
return jsonify(files), 200
|
||||
@@ -362,7 +363,7 @@ def list_view(name, token = None):
|
||||
return redirect(url_for('list_view',name=name))
|
||||
|
||||
files = []
|
||||
for file in iter_folder_files(share['path']):
|
||||
for file in iter_folder_files(share['path'], version_folder = app.config['VERSION_FOLDER']):
|
||||
status = file_stat(share['path'],file)
|
||||
status.update({
|
||||
'token': get_direct_token(share, file),
|
||||
@@ -514,7 +515,7 @@ def script_download(name = None, token = None):
|
||||
if not ok:
|
||||
return share
|
||||
commands = []
|
||||
for file in iter_folder_files(share['path']):
|
||||
for file in iter_folder_files(share['path'], version_folder = app.config['VERSION_FOLDER']):
|
||||
status = file_stat(share['path'], file)
|
||||
commands.append('get_file "%s"'%(
|
||||
status['url'],
|
||||
@@ -534,7 +535,7 @@ def script_direct(name = None, token = None):
|
||||
if not ok:
|
||||
return share
|
||||
commands = []
|
||||
for file in iter_folder_files(share['path']):
|
||||
for file in iter_folder_files(share['path'], version_folder = app.config['VERSION_FOLDER']):
|
||||
status = file_stat(share['path'], file)
|
||||
commands.append('get_file "%s" "%s"'%(
|
||||
status['url'],
|
||||
@@ -695,22 +696,20 @@ def file_versionize(full_path):
|
||||
""" Move file to versioned with integer """
|
||||
file_dir = os.path.dirname(full_path)
|
||||
file_name = os.path.basename(full_path)
|
||||
basename, extension = os.path.splitext(file_name)
|
||||
version = 1
|
||||
while True:
|
||||
new_name = os.path.join(
|
||||
file_dir,
|
||||
secure_filename("%s.v%d%s"%(
|
||||
basename,
|
||||
version,
|
||||
extension
|
||||
))
|
||||
)
|
||||
if os.path.exists(new_name):
|
||||
version += 1
|
||||
else:
|
||||
break
|
||||
os.rename(full_path,new_name)
|
||||
new_name = file_name_version(full_path)
|
||||
new_path = os.path.join(
|
||||
file_dir,
|
||||
app.config['VERSION_FOLDER'],
|
||||
new_name
|
||||
)
|
||||
|
||||
if os.path.exists(new_path):
|
||||
return
|
||||
makedirs_rights(os.path.join(
|
||||
file_dir,
|
||||
app.config['VERSION_FOLDER']
|
||||
))
|
||||
os.rename(full_path, new_path)
|
||||
|
||||
|
||||
def get_share(name, require_auth = True, token = None):
|
||||
@@ -809,7 +808,7 @@ def zip_share(share):
|
||||
)
|
||||
)
|
||||
zf = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
|
||||
for file in iter_folder_files(share['path']):
|
||||
for file in iter_folder_files(share['path'], version_folder = app.config['VERSION_FOLDER']):
|
||||
fp = os.path.join(share['path'], file)
|
||||
if os.path.isdir(fp):
|
||||
continue
|
||||
|
||||
@@ -93,6 +93,68 @@ def list_folders(shares,config):
|
||||
print(tabulate(table, headers = "firstrow"))
|
||||
|
||||
|
||||
def list_versions(shares, config, opts):
|
||||
if 'data_folder' not in config:
|
||||
print("data_folder not defined in config")
|
||||
sys.exit(1)
|
||||
data_folder = os.path.join(config['__root_path__'], config['data_folder'])
|
||||
table = []
|
||||
header = ['Share', 'Path', 'File', 'Size', 'Unit']
|
||||
if opts.delete == None:
|
||||
header.append('Age')
|
||||
else:
|
||||
header.append('Delete')
|
||||
table.append(header)
|
||||
now = datetime.now()
|
||||
for share in shares:
|
||||
if opts.name:
|
||||
if share['name'] != opts.name:
|
||||
continue
|
||||
version_folder = os.path.join(
|
||||
data_folder,
|
||||
share['path'],
|
||||
config['version_folder']
|
||||
)
|
||||
if not os.path.isdir(version_folder):
|
||||
table.append((
|
||||
share['name'],
|
||||
share['path']+'/',
|
||||
'-', '-', '-', '-'
|
||||
))
|
||||
else:
|
||||
for filename in sorted(os.listdir(version_folder)):
|
||||
full_path = os.path.join(version_folder, filename)
|
||||
if os.path.isdir(full_path):
|
||||
size = get_folder_size(full_path)
|
||||
else:
|
||||
size = os.path.getsize(full_path)
|
||||
(size_num, size_unit) = file_size_human(
|
||||
size,
|
||||
HTML=False
|
||||
).split(" ",1)
|
||||
parsed_date = version_date(filename)
|
||||
if parsed_date == None:
|
||||
age_str = '-'
|
||||
else:
|
||||
age = now - parsed_date
|
||||
age_str = "%d d"%( age.days, )
|
||||
if opts.delete != None:
|
||||
to_delete = age.days >= opts.delete
|
||||
age_str = "To Del" if to_delete else "Keep"
|
||||
if not opts.dry and to_delete:
|
||||
age_str = "Deleted"
|
||||
os.remove(full_path)
|
||||
table.append((
|
||||
share['name'],
|
||||
share['path'] + '/',
|
||||
filename,
|
||||
size_num,
|
||||
size_unit,
|
||||
age_str
|
||||
))
|
||||
print(tabulate(table, headers = "firstrow"))
|
||||
|
||||
|
||||
def add_share(shares, config, opts):
|
||||
|
||||
# Make name and path safe:
|
||||
@@ -408,7 +470,7 @@ def print_rest_api_download(config, share, token, show_filename):
|
||||
if not os.path.exists(share_path):
|
||||
print("no files")
|
||||
sys.exit(0)
|
||||
for filename in iter_folder_files(share_path):
|
||||
for filename in iter_folder_files(share_path, version_folder = config['version_folder']):
|
||||
if show_filename:
|
||||
if filename != show_filename:
|
||||
continue
|
||||
@@ -441,7 +503,7 @@ def print_rest_api_direct(config, share, token, show_filename):
|
||||
if not os.path.exists(share_path):
|
||||
print("no files")
|
||||
sys.exit(0)
|
||||
for filename in iter_folder_files(share_path):
|
||||
for filename in iter_folder_files(share_path, version_folder = config['version_folder']):
|
||||
if show_filename:
|
||||
if filename != show_filename:
|
||||
continue
|
||||
@@ -540,6 +602,14 @@ def parse_options():
|
||||
parser_list = subparsers.add_parser('list', help = "List shares")
|
||||
## list folders
|
||||
parser_folders = subparsers.add_parser('folders', help = "List the subfolders in data folder, and their disk usage")
|
||||
## list versions
|
||||
parser_versions = subparsers.add_parser('versions', help = "List the old versions stored in shares, and their disk usage")
|
||||
parser_versions.add_argument('--delete', action="store", dest="delete", default = None, type = int,
|
||||
help = "Delete old versions, older than N days.")
|
||||
parser_versions.add_argument('--dry', action="store_true", dest="dry", default = False,
|
||||
help = "Do not actually delete files.")
|
||||
parser_versions.add_argument('-n','--name', action="store", dest="name", required = False, default = None,
|
||||
help = "Show / Delete only this share versions. If omitted, applies to all shares")
|
||||
## Show
|
||||
parser_show = subparsers.add_parser('show', help = "Show share")
|
||||
parser_show.add_argument('-P', action="store_true", dest="show_password", default = False,
|
||||
@@ -655,6 +725,8 @@ if __name__ == "__main__":
|
||||
|
||||
if opts.subparser_name == 'list':
|
||||
list_shares(shares,opts)
|
||||
if opts.subparser_name == 'versions':
|
||||
list_versions(shares,config,opts)
|
||||
elif opts.subparser_name == 'folders':
|
||||
list_folders(shares,config)
|
||||
elif opts.subparser_name == 'show':
|
||||
|
||||
@@ -272,3 +272,9 @@ tr:nth-child(odd) {
|
||||
.sortarrow {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
/* editor */
|
||||
|
||||
.href_button {
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
@@ -155,9 +155,6 @@ function changeTitle(newTitle) {
|
||||
document.title = "Flees - " + newTitle;
|
||||
}
|
||||
|
||||
function back() {
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
function keyboardEntry(ev){
|
||||
var kC=ev.keyCode;
|
||||
@@ -168,6 +165,9 @@ function keyboardEntry(ev){
|
||||
if (/T/.test(k)) {
|
||||
infoToggle();
|
||||
}
|
||||
if (/C/.test(k)) {
|
||||
scriptToggle();
|
||||
}
|
||||
}
|
||||
|
||||
document.onkeyup = keyboardEntry;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<div id=index_menu>
|
||||
Add text file to {{ name|safe }}
|
||||
Edit text file in share: <a href="{{ url_for('list_view',name = name) }}">{{ name|safe }}</a>
|
||||
<div id=editor_form>
|
||||
<form action={{ url_for('paste',name=name) }} method=post enctype=multipart/form-data>
|
||||
<input type=hidden name=from_gui value="true" />
|
||||
<input id="paste_filename" type=text name=filename
|
||||
title="File name to write"
|
||||
title="File name to write. Must end in .txt to later edit."
|
||||
value="{{ filename | safe }}" onclick="clear_text(this.id,'paste.txt')">
|
||||
<br>
|
||||
<input type=submit value=Submit>
|
||||
<input type=submit value=Cancel onclick="back()">
|
||||
<a class="href_button" href="{{ url_for('list_view',name = name) }}">Cancel</a>
|
||||
<br>
|
||||
<textarea rows="25" cols="80" name="paste" id="paste_paste" autofocus>{{ content | safe }}</textarea>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">changeTitle("{{ name | safe }}");</script>
|
||||
<script type="text/javascript">changeTitle("{{ name | safe }} - Editor");</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<li><span title="Uploads not enabled for this sahre">No uploading</span>
|
||||
{% endif %}
|
||||
<li><a href="{{ url_for('download_zip',name=name) }}" title="Download all the files as one ZIP file. Total size of files must be less than {{ g.max_zip_size }} Mb">Download as zip</a>
|
||||
<li><span id=list_script_toggle onclick="scriptToggle()" title="Show command line usage information">Command line info</span>
|
||||
<li><span id=list_script_toggle onclick="scriptToggle()" title="Show command line usage information, keyboard: c">Command line info</span>
|
||||
<li><a href="{{ url_for('editor', name = name) }}">Paste or write a file</a>
|
||||
<li><a href="{{ url_for('index') }}">Return to index</a>
|
||||
<li><a href="{{ url_for('logout',name=name) }}">Logout</a>
|
||||
@@ -77,7 +77,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_gui', name = name, filename = entry.url) }}">{{ entry.name }}</a>
|
||||
<a href="{{ url_for('download_gui', name = name, filename = entry.name ) }}">{{ entry.name }}</a>
|
||||
{% if upload %}
|
||||
{% if entry.editable %}
|
||||
<form class="edit_form" action="{{ url_for('editor') }}" method=post>
|
||||
|
||||
@@ -29,6 +29,36 @@ def file_date_human(num):
|
||||
).strftime(app.config['DATE_FORMAT'])
|
||||
|
||||
|
||||
def file_name_version(full_path):
|
||||
""" New name versioned with date of the file """
|
||||
file_dir = os.path.dirname(full_path)
|
||||
file_name = os.path.basename(full_path)
|
||||
file_time = os.stat(full_path).st_mtime
|
||||
time_formatted = datetime.fromtimestamp(
|
||||
file_time
|
||||
).strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
new_name = '%s.%s'%(
|
||||
time_formatted,
|
||||
file_name
|
||||
)
|
||||
return new_name
|
||||
|
||||
|
||||
def version_date(full_path):
|
||||
""" Date of versioned file """
|
||||
file_dir = os.path.dirname(full_path)
|
||||
file_name = os.path.basename(full_path)
|
||||
try:
|
||||
return datetime.strptime(
|
||||
file_name[0:15],
|
||||
'%Y%m%d_%H%M%S'
|
||||
)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def file_stat(path, filename):
|
||||
full_path = os.path.join(path, filename)
|
||||
s = os.stat(full_path)
|
||||
@@ -110,9 +140,12 @@ def is_valid_url(url, qualifying = None):
|
||||
for qualifying_attr in qualifying])
|
||||
|
||||
|
||||
def iter_folder_files(path, recursive = True):
|
||||
def iter_folder_files(path, recursive = True, version_folder = None):
|
||||
if recursive:
|
||||
for dirpath, dirnames, filenames in os.walk(path, topdown = False):
|
||||
path_list = dirpath.split(os.sep)
|
||||
if path_list[-1] == version_folder:
|
||||
continue
|
||||
relative_path = os.path.relpath(dirpath,path)
|
||||
dirnames.sort()
|
||||
if "/." in relative_path:
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"__do_not_edit": "most likely you will not change anything after this line",
|
||||
"data_folder": "data",
|
||||
"shares_file": "data/shares.json",
|
||||
"version_folder": "_version",
|
||||
"zip_folder": "data/.zip",
|
||||
"date_format": "%Y-%m-%d %H:%M",
|
||||
"debug": false
|
||||
|
||||
Reference in New Issue
Block a user