Started implement server side filechooser
This commit is contained in:
parent
983e3b2274
commit
2508c1abb2
86
cps/admin.py
86
cps/admin.py
|
@ -5,7 +5,7 @@
|
||||||
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
|
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
|
||||||
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
|
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
|
||||||
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
|
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
|
||||||
# apetresc, nanu-c, mutschler
|
# apetresc, nanu-c, mutschler, GammaC0de, vuolter
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -26,6 +26,7 @@ import re
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import operator
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from babel import Locale as LC
|
from babel import Locale as LC
|
||||||
|
@ -520,6 +521,89 @@ def list_restriction(res_type):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@admi.route("/ajax/pathchooser/", endpoint="pathchooser")
|
||||||
|
@admi.route("/ajax/filechooser/", endpoint="filechooser")
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def pathchooser():
|
||||||
|
browse_for = "folder" if request.endpoint == "admin.pathchooser" else "file"
|
||||||
|
path = os.path.normpath(request.args.get('path', ""))
|
||||||
|
|
||||||
|
if os.path.isfile(path):
|
||||||
|
oldfile = path
|
||||||
|
path = os.path.dirname(path)
|
||||||
|
else:
|
||||||
|
oldfile = ""
|
||||||
|
|
||||||
|
abs = False
|
||||||
|
|
||||||
|
if os.path.isdir(path):
|
||||||
|
if os.path.isabs(path):
|
||||||
|
cwd = os.path.realpath(path)
|
||||||
|
abs = True
|
||||||
|
else:
|
||||||
|
cwd = os.path.relpath(path)
|
||||||
|
else:
|
||||||
|
cwd = os.getcwd()
|
||||||
|
|
||||||
|
cwd = os.path.normpath(os.path.realpath(cwd))
|
||||||
|
parentdir = os.path.dirname(cwd)
|
||||||
|
if not abs:
|
||||||
|
if os.path.realpath(cwd) == os.path.realpath("/"):
|
||||||
|
cwd = os.path.relpath(cwd)
|
||||||
|
else:
|
||||||
|
cwd = os.path.relpath(cwd) + os.path.sep
|
||||||
|
parentdir = os.path.relpath(parentdir) + os.path.sep
|
||||||
|
|
||||||
|
if os.path.realpath(cwd) == os.path.realpath("/"):
|
||||||
|
parentdir = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
folders = os.listdir(cwd)
|
||||||
|
except Exception:
|
||||||
|
folders = []
|
||||||
|
|
||||||
|
files = []
|
||||||
|
locale = get_locale()
|
||||||
|
for f in folders:
|
||||||
|
try:
|
||||||
|
data = {"name": f, "fullpath": os.path.join(cwd, f)}
|
||||||
|
data["sort"] = data["fullpath"].lower()
|
||||||
|
data["modified"] = format_datetime(datetime.fromtimestamp(int(os.path.getmtime(os.path.join(cwd, f)))),
|
||||||
|
format='short', locale=locale)
|
||||||
|
data["ext"] = os.path.splitext(f)[1]
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if os.path.isfile(os.path.join(cwd, f)):
|
||||||
|
data["type"] = "file"
|
||||||
|
data["size"] = os.path.getsize(os.path.join(cwd, f))
|
||||||
|
|
||||||
|
power = 0
|
||||||
|
while (data["size"] >> 10) > 0.3:
|
||||||
|
power += 1
|
||||||
|
data["size"] >>= 10
|
||||||
|
units = ("", "K", "M", "G", "T")
|
||||||
|
data["size"] = str(data["size"]) + " " + units[power] + "Byte"
|
||||||
|
else:
|
||||||
|
data["type"] = "dir"
|
||||||
|
data["size"] = ""
|
||||||
|
|
||||||
|
files.append(data)
|
||||||
|
|
||||||
|
files = sorted(files, key=operator.itemgetter("type", "sort"))
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"cwd": cwd,
|
||||||
|
"files": files,
|
||||||
|
"parentdir": parentdir,
|
||||||
|
"type": browse_for,
|
||||||
|
"oldfile": oldfile,
|
||||||
|
"absolute": abs,
|
||||||
|
}
|
||||||
|
return json.dumps(context)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/config", methods=["GET", "POST"])
|
@admi.route("/config", methods=["GET", "POST"])
|
||||||
@unconfigured
|
@unconfigured
|
||||||
def basic_configuration():
|
def basic_configuration():
|
||||||
|
|
|
@ -37,7 +37,7 @@ from flask_login import login_required
|
||||||
from . import logger, gdriveutils, config, ub, calibre_db
|
from . import logger, gdriveutils, config, ub, calibre_db
|
||||||
from .web import admin_required
|
from .web import admin_required
|
||||||
|
|
||||||
gdrive = Blueprint('gdrive', __name__)
|
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -50,7 +50,7 @@ current_milli_time = lambda: int(round(time() * 1000))
|
||||||
gdrive_watch_callback_token = 'target=calibreweb-watch_files'
|
gdrive_watch_callback_token = 'target=calibreweb-watch_files'
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/gdrive/authenticate")
|
@gdrive.route("/authenticate")
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def authenticate_google_drive():
|
def authenticate_google_drive():
|
||||||
|
@ -63,7 +63,7 @@ def authenticate_google_drive():
|
||||||
return redirect(authUrl)
|
return redirect(authUrl)
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/gdrive/callback")
|
@gdrive.route("/callback")
|
||||||
def google_drive_callback():
|
def google_drive_callback():
|
||||||
auth_code = request.args.get('code')
|
auth_code = request.args.get('code')
|
||||||
if not auth_code:
|
if not auth_code:
|
||||||
|
@ -77,19 +77,14 @@ def google_drive_callback():
|
||||||
return redirect(url_for('admin.configuration'))
|
return redirect(url_for('admin.configuration'))
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/gdrive/watch/subscribe")
|
@gdrive.route("/watch/subscribe")
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def watch_gdrive():
|
def watch_gdrive():
|
||||||
if not config.config_google_drive_watch_changes_response:
|
if not config.config_google_drive_watch_changes_response:
|
||||||
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
|
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
|
||||||
filedata = json.load(settings)
|
filedata = json.load(settings)
|
||||||
# ToDo: Easier: rstrip('/')
|
address = filedata['web']['redirect_uris'][0].rstrip('/').replace('/gdrive/callback', '/gdrive/watch/callback')
|
||||||
if filedata['web']['redirect_uris'][0].endswith('/'):
|
|
||||||
filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-((len('/gdrive/callback')+1))]
|
|
||||||
else:
|
|
||||||
filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-(len('/gdrive/callback'))]
|
|
||||||
address = '%s/gdrive/watch/callback' % filedata['web']['redirect_uris'][0]
|
|
||||||
notification_id = str(uuid4())
|
notification_id = str(uuid4())
|
||||||
try:
|
try:
|
||||||
result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id,
|
result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id,
|
||||||
|
@ -99,14 +94,15 @@ def watch_gdrive():
|
||||||
except HttpError as e:
|
except HttpError as e:
|
||||||
reason=json.loads(e.content)['error']['errors'][0]
|
reason=json.loads(e.content)['error']['errors'][0]
|
||||||
if reason['reason'] == u'push.webhookUrlUnauthorized':
|
if reason['reason'] == u'push.webhookUrlUnauthorized':
|
||||||
flash(_(u'Callback domain is not verified, please follow steps to verify domain in google developer console'), category="error")
|
flash(_(u'Callback domain is not verified, '
|
||||||
|
u'please follow steps to verify domain in google developer console'), category="error")
|
||||||
else:
|
else:
|
||||||
flash(reason['message'], category="error")
|
flash(reason['message'], category="error")
|
||||||
|
|
||||||
return redirect(url_for('admin.configuration'))
|
return redirect(url_for('admin.configuration'))
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/gdrive/watch/revoke")
|
@gdrive.route("/watch/revoke")
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def revoke_watch_gdrive():
|
def revoke_watch_gdrive():
|
||||||
|
@ -122,14 +118,14 @@ def revoke_watch_gdrive():
|
||||||
return redirect(url_for('admin.configuration'))
|
return redirect(url_for('admin.configuration'))
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST'])
|
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
|
||||||
def on_received_watch_confirmation():
|
def on_received_watch_confirmation():
|
||||||
if not config.config_google_drive_watch_changes_response:
|
if not config.config_google_drive_watch_changes_response:
|
||||||
return ''
|
return ''
|
||||||
if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \
|
if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \
|
||||||
or request.headers.get('X-Goog-Resource-State') != 'change' \
|
or request.headers.get('X-Goog-Resource-State') != 'change' \
|
||||||
or not request.data:
|
or not request.data:
|
||||||
return redirect(url_for('admin.configuration'))
|
return '' # redirect(url_for('admin.configuration'))
|
||||||
|
|
||||||
log.debug('%r', request.headers)
|
log.debug('%r', request.headers)
|
||||||
log.debug('%r', request.data)
|
log.debug('%r', request.data)
|
||||||
|
|
|
@ -213,6 +213,45 @@ $(function() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fillFileTable(path, type) {
|
||||||
|
if (type === "dir") {
|
||||||
|
var request_path = "/../../ajax/pathchooser/";
|
||||||
|
} else {
|
||||||
|
var request_path = "/../../ajax/filechooser/";
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
dataType: "json",
|
||||||
|
data: {
|
||||||
|
path: path,
|
||||||
|
},
|
||||||
|
url: window.location.pathname + request_path,
|
||||||
|
success: function success(data) {
|
||||||
|
$("#file_table > tbody > tr").each(function () {
|
||||||
|
if ($(this).attr("id") !== "parent") {
|
||||||
|
$(this).closest("tr").remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (data.parentdir !== "") {
|
||||||
|
$("#parent").removeClass('hidden')
|
||||||
|
} else {
|
||||||
|
$("#parent").addClass('hidden')
|
||||||
|
}
|
||||||
|
// console.log(data);
|
||||||
|
data.files.forEach(function(entry) {
|
||||||
|
if(entry.type === "dir") {
|
||||||
|
var type = "<span class=\"glyphicon glyphicon-folder-close\"></span>";
|
||||||
|
} else {
|
||||||
|
var type = "";
|
||||||
|
}
|
||||||
|
$("<tr class=\"tr-clickable\" data-type=\"" + entry.type + "\" data-path=\"" +
|
||||||
|
entry.fullpath + "\"><td>" + type + "</td><td>" + entry.name + "</td><td>" +
|
||||||
|
entry.size + "</td></tr>").appendTo($("#file_table"));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
timeout: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$(".discover .row").isotope({
|
$(".discover .row").isotope({
|
||||||
// options
|
// options
|
||||||
itemSelector : ".book",
|
itemSelector : ".book",
|
||||||
|
@ -402,6 +441,81 @@ $(function() {
|
||||||
$("#config_delete_kobo_token").show();
|
$("#config_delete_kobo_token").show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$("#fileModal").on("show.bs.modal", function(e) {
|
||||||
|
//get data-id attribute of the clicked element and store in button
|
||||||
|
//var submit = true;
|
||||||
|
//var cwd = "{{oldfile|default(cwd, True)|abspath|replace('\\', '\\\\')}}";
|
||||||
|
//var isabsolute = true;
|
||||||
|
fillFileTable("","dir");
|
||||||
|
});
|
||||||
|
|
||||||
|
//(".tr-clickable").on("click",
|
||||||
|
$(document).on("click", ".tr-clickable", function() {
|
||||||
|
var path = this.attributes['data-path'].value;
|
||||||
|
var type = this.attributes['data-type'].value;
|
||||||
|
fillFileTable(path, type);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*{% if type == 'folder' %} {# browsing for folder #}
|
||||||
|
var abspath = "{{url_for('app.pathchooser') + '?path=' + cwd|abspath|quote_plus}}";
|
||||||
|
var relpath = "{{url_for('app.pathchooser') + '?path=' + cwd|relpath|quote_plus}}";
|
||||||
|
{% else %} {# browsing for file #}
|
||||||
|
var abspath = "{{url_for('app.filechooser') + '?path=' + oldfile|default(cwd, True)|abspath|quote_plus}}";
|
||||||
|
var relpath = "{{url_for('app.filechooser') + '?path=' + oldfile|default(cwd, True)|relpath|quote_plus}}";
|
||||||
|
{% endif %}*/
|
||||||
|
/*document.addEventListener("readystatechange", function(event) {
|
||||||
|
if (this.readyState === "complete") {
|
||||||
|
document.getElementById("tbody").style.height = (window.innerHeight - 25) + "px";
|
||||||
|
window.onresize = function (event) {
|
||||||
|
document.getElementById("tbody").style.height = (window.innerHeight - 25) + "px";
|
||||||
|
};
|
||||||
|
var clickables = document.getElementsByClassName("tr-clickable");
|
||||||
|
for (var i = 0; i < clickables.length; i++) {
|
||||||
|
clickables[i].onclick = (function () {
|
||||||
|
var onclick = clickables[i].onclick;
|
||||||
|
return function (e) {
|
||||||
|
if (onclick != null && !onclick()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this.dataset.href !== undefined && this.dataset.href !== "#") {
|
||||||
|
window.location.href = this.dataset.href;
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function updateParent()
|
||||||
|
{
|
||||||
|
if (window.top.SettingsUI !== undefined) {
|
||||||
|
window.top.SettingsUI.prototype.pathchooserChanged(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setInvalid() {
|
||||||
|
submit = false;
|
||||||
|
cwd = "";
|
||||||
|
updateParent();
|
||||||
|
}
|
||||||
|
function setValid() {
|
||||||
|
submit = true;
|
||||||
|
updateParent();
|
||||||
|
}
|
||||||
|
function setFile(fullpath, name)
|
||||||
|
{
|
||||||
|
cwd = fullpath;
|
||||||
|
/*{*% if type == "file" %} {# browsing for file #}
|
||||||
|
abspath = "{{url_for('app.filechooser')}}?path={{cwd|abspath|quote_plus}}" + encodeURIComponent(name);
|
||||||
|
relpath = "{{url_for('app.filechooser')}}?path={{cwd|relpath|quote_plus}}" + encodeURIComponent(name);
|
||||||
|
{% endif %}*/
|
||||||
|
/*setValid();
|
||||||
|
}*/
|
||||||
|
|
||||||
$("#btndeletetoken").click(function() {
|
$("#btndeletetoken").click(function() {
|
||||||
//get data-id attribute of the clicked element
|
//get data-id attribute of the clicked element
|
||||||
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
|
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
|
||||||
|
|
|
@ -384,7 +384,7 @@
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" class="form-control" id="config_converterpath" name="config_converterpath" value="{% if config.config_converterpath != None %}{{ config.config_converterpath }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" id="config_converterpath" name="config_converterpath" value="{% if config.config_converterpath != None %}{{ config.config_converterpath }}{% endif %}" autocomplete="off">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="converter_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
<button type="button" id="converter_modal_path" data-toggle="modal" data-target="#fileModal" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -412,8 +412,6 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
{% if not show_login_button %}
|
{% if not show_login_button %}
|
||||||
<button type="submit" name="submit" class="btn btn-default">{{_('Save')}}</button>
|
<button type="submit" name="submit" class="btn btn-default">{{_('Save')}}</button>
|
||||||
|
@ -428,6 +426,9 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block modal %}
|
||||||
|
{{ filechooser_modal() }}
|
||||||
|
{% endblock %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(document).on('change', '#config_use_google_drive', function() {
|
$(document).on('change', '#config_use_google_drive', function() {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% from 'modal_dialogs.html' import restrict_modal, delete_book %}
|
{% from 'modal_dialogs.html' import restrict_modal, delete_book, filechooser_modal %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ g.user.locale }}">
|
<html lang="{{ g.user.locale }}">
|
||||||
<head>
|
<head>
|
||||||
|
|
|
@ -68,3 +68,36 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
{% macro filechooser_modal() %}
|
||||||
|
<div class="modal fade" id="fileModal" role="dialog" aria-labelledby="metafileLabel">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-info text-center">
|
||||||
|
<span>{{_('Choose File Location')}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<table id="file_table" class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{_('type')}}</th>
|
||||||
|
<th>{{_('name')}}</th>
|
||||||
|
<th>{{_('size')}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tbody">
|
||||||
|
<tr class="tr-clickable hidden" id="parent" data-type="dir" data-path="..">
|
||||||
|
<td><span class="glyphicon glyphicon-folder-close"></span></td>
|
||||||
|
<td title="{{_('Parent Directory')}}"><span class="parentdir">..</span></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input type="button" class="btn btn-primary" value="{{_('Select')}}" name="file_confirm" id="file_confirm" data-dismiss="modal">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user