Started implement server side filechooser

pull/1768/head
Ozzieisaacs 4 years ago
parent 983e3b2274
commit 2508c1abb2

@ -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…
Cancel
Save