mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 09:02:15 -06:00
markdown editor works
This commit is contained in:
parent
706f30033e
commit
d9c71c11fd
|
@ -35,7 +35,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||
* [x] volumes
|
||||
* [x] accounts
|
||||
* [x] markdown viewer
|
||||
* [ ] markdown editor? w
|
||||
* [x] markdown editor
|
||||
|
||||
summary: it works! you can use it! (but technically not even close to beta)
|
||||
|
||||
|
|
|
@ -45,6 +45,11 @@ class HttpCli(object):
|
|||
def _check_nonfatal(self, ex):
|
||||
return ex.code in [403, 404]
|
||||
|
||||
def _assert_safe_rem(self, rem):
|
||||
# sanity check to prevent any disasters
|
||||
if rem.startswith("/") or rem.startswith("../") or "/../" in rem:
|
||||
raise Exception("that was close")
|
||||
|
||||
def run(self):
|
||||
"""returns true if connection can be reused"""
|
||||
self.keepalive = False
|
||||
|
@ -263,9 +268,16 @@ class HttpCli(object):
|
|||
if act == "mkdir":
|
||||
return self.handle_mkdir()
|
||||
|
||||
if act == "new_md":
|
||||
# kinda silly but has the least side effects
|
||||
return self.handle_new_md()
|
||||
|
||||
if act == "bput":
|
||||
return self.handle_plain_upload()
|
||||
|
||||
if act == "tput":
|
||||
return self.handle_text_upload()
|
||||
|
||||
raise Pebkac(422, 'invalid action "{}"'.format(act))
|
||||
|
||||
def handle_post_json(self):
|
||||
|
@ -407,11 +419,7 @@ class HttpCli(object):
|
|||
|
||||
nullwrite = self.args.nw
|
||||
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
||||
|
||||
# rem is escaped at this point,
|
||||
# this is just a sanity check to prevent any disasters
|
||||
if rem.startswith("/") or rem.startswith("../") or "/../" in rem:
|
||||
raise Exception("that was close")
|
||||
self._assert_safe_rem(rem)
|
||||
|
||||
if not nullwrite:
|
||||
fdir = os.path.join(vfs.realpath, rem)
|
||||
|
@ -429,12 +437,44 @@ class HttpCli(object):
|
|||
raise Pebkac(500, "mkdir failed, check the logs")
|
||||
|
||||
vpath = "{}/{}".format(self.vpath, new_dir).lstrip("/")
|
||||
redir = '<script>document.getElementsByTagName("a")[0].click()</script>'
|
||||
html = self.conn.tpl_msg.render(
|
||||
h2='<a href="/{}">go to /{}</a>{}'.format(
|
||||
quotep(vpath), html_escape(vpath, quote=False), redir
|
||||
h2='<a href="/{}">go to /{}</a>'.format(
|
||||
quotep(vpath), html_escape(vpath, quote=False)
|
||||
),
|
||||
pre="aight",
|
||||
click=True,
|
||||
)
|
||||
self.reply(html.encode("utf-8", "replace"))
|
||||
return True
|
||||
|
||||
def handle_new_md(self):
|
||||
new_file = self.parser.require("name", 512)
|
||||
self.parser.drop()
|
||||
|
||||
nullwrite = self.args.nw
|
||||
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
||||
self._assert_safe_rem(rem)
|
||||
|
||||
if not new_file.endswith(".md"):
|
||||
new_file += ".md"
|
||||
|
||||
if not nullwrite:
|
||||
fdir = os.path.join(vfs.realpath, rem)
|
||||
fn = os.path.join(fdir, sanitize_fn(new_file))
|
||||
|
||||
if os.path.exists(fsenc(fn)):
|
||||
raise Pebkac(500, "that file exists already")
|
||||
|
||||
with open(fsenc(fn), "wb") as f:
|
||||
f.write(b"`GRUNNUR`\n")
|
||||
|
||||
vpath = "{}/{}".format(self.vpath, new_file).lstrip("/")
|
||||
html = self.conn.tpl_msg.render(
|
||||
h2='<a href="/{}?edit">go to /{}?edit</a>'.format(
|
||||
quotep(vpath), html_escape(vpath, quote=False)
|
||||
),
|
||||
pre="aight",
|
||||
click=True,
|
||||
)
|
||||
self.reply(html.encode("utf-8", "replace"))
|
||||
return True
|
||||
|
@ -442,11 +482,7 @@ class HttpCli(object):
|
|||
def handle_plain_upload(self):
|
||||
nullwrite = self.args.nw
|
||||
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
||||
|
||||
# rem is escaped at this point,
|
||||
# this is just a sanity check to prevent any disasters
|
||||
if rem.startswith("/") or rem.startswith("../") or "/../" in rem:
|
||||
raise Exception("that was close")
|
||||
self._assert_safe_rem(rem)
|
||||
|
||||
files = []
|
||||
errmsg = ""
|
||||
|
@ -537,6 +573,86 @@ class HttpCli(object):
|
|||
self.parser.drop()
|
||||
return True
|
||||
|
||||
def handle_text_upload(self):
|
||||
try:
|
||||
cli_lastmod3 = int(self.parser.require("lastmod", 16))
|
||||
except:
|
||||
raise Pebkac(400, "could not read lastmod from request")
|
||||
|
||||
nullwrite = self.args.nw
|
||||
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
||||
self._assert_safe_rem(rem)
|
||||
|
||||
# TODO:
|
||||
# the per-volume read/write permissions must be replaced with permission flags
|
||||
# which would decide how to handle uploads to filenames which are taken,
|
||||
# current behavior of creating a new name is a good default for binary files
|
||||
# but should also offer a flag to takeover the filename and rename the old one
|
||||
#
|
||||
# stopgap:
|
||||
if not rem.endswith(".md"):
|
||||
raise Pebkac(400, "only markdown pls")
|
||||
|
||||
if nullwrite:
|
||||
response = json.dumps({"ok": True, "lastmod": 0})
|
||||
self.log(response)
|
||||
# TODO reply should parser.drop()
|
||||
self.parser.drop()
|
||||
self.reply(response.encode("utf-8"))
|
||||
return True
|
||||
|
||||
fn = os.path.join(vfs.realpath, rem)
|
||||
srv_lastmod = -1
|
||||
try:
|
||||
st = os.stat(fsenc(fn))
|
||||
srv_lastmod = st.st_mtime
|
||||
srv_lastmod3 = int(srv_lastmod * 1000)
|
||||
except OSError as ex:
|
||||
if ex.errno != 2:
|
||||
raise
|
||||
|
||||
# if file exists, chekc that timestamp matches the client's
|
||||
if srv_lastmod >= 0:
|
||||
if cli_lastmod3 not in [-1, srv_lastmod3]:
|
||||
response = json.dumps(
|
||||
{
|
||||
"ok": False,
|
||||
"lastmod": srv_lastmod3,
|
||||
"now": int(time.time() * 1000),
|
||||
}
|
||||
)
|
||||
self.log(
|
||||
"{} - {} = {}".format(
|
||||
srv_lastmod3, cli_lastmod3, srv_lastmod3 - cli_lastmod3
|
||||
)
|
||||
)
|
||||
self.log(response)
|
||||
self.parser.drop()
|
||||
self.reply(response.encode("utf-8"))
|
||||
return True
|
||||
|
||||
# TODO another hack re: pending permissions rework
|
||||
os.rename(fn, "{}.bak-{:.3f}.md".format(fn[:-3], srv_lastmod))
|
||||
|
||||
p_field, _, p_data = next(self.parser.gen)
|
||||
if p_field != "body":
|
||||
raise Pebkac(400, "expected body, got {}".format(p_field))
|
||||
|
||||
with open(fn, "wb") as f:
|
||||
sz, sha512, _ = hashcopy(self.conn, p_data, f)
|
||||
|
||||
new_lastmod = os.stat(fsenc(fn)).st_mtime
|
||||
new_lastmod3 = int(new_lastmod * 1000)
|
||||
sha512 = sha512[:56]
|
||||
|
||||
response = json.dumps(
|
||||
{"ok": True, "lastmod": new_lastmod3, "size": sz, "sha512": sha512}
|
||||
)
|
||||
self.log(response)
|
||||
self.parser.drop()
|
||||
self.reply(response.encode("utf-8"))
|
||||
return True
|
||||
|
||||
def _chk_lastmod(self, file_ts):
|
||||
file_dt = datetime.utcfromtimestamp(file_ts)
|
||||
file_lastmod = file_dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||
|
@ -731,6 +847,7 @@ class HttpCli(object):
|
|||
|
||||
targs = {
|
||||
"title": html_escape(self.vpath, quote=False),
|
||||
"lastmod": int(ts_md * 1000),
|
||||
"md": "",
|
||||
}
|
||||
sz_html = len(template.render(**targs).encode("utf-8"))
|
||||
|
|
|
@ -45,6 +45,14 @@ html, body {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
html .editor-toolbar>button.disabled {
|
||||
opacity: .35;
|
||||
pointer-events: none;
|
||||
}
|
||||
html .editor-toolbar>button.save.force-save {
|
||||
background: #f97;
|
||||
}
|
||||
|
||||
/*
|
||||
*[data-ln]:before {
|
||||
content: attr(data-ln);
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<script>
|
||||
|
||||
var link_md_as_html = false; // TODO (does nothing)
|
||||
var last_modified = {{ lastmod }};
|
||||
|
||||
</script>
|
||||
<script src="/.cpr/deps/easymde.full.js"></script>
|
||||
|
|
|
@ -24,15 +24,14 @@ var dom_md = document.getElementById('mt');
|
|||
name: "save",
|
||||
title: "save",
|
||||
className: "fa fa-save",
|
||||
action: (mde) => {
|
||||
alert("TODO save (" + (mde.value().length / 1024) + ' kB)');
|
||||
}
|
||||
action: save
|
||||
}, '|',
|
||||
'bold', 'italic', 'strikethrough', 'heading', '|',
|
||||
'code', 'quote', 'unordered-list', 'ordered-list', 'clean-block', '|',
|
||||
'link', 'image', 'table', 'horizontal-rule', '|',
|
||||
'preview', 'side-by-side', 'fullscreen', '|',
|
||||
'undo', 'redo'];
|
||||
|
||||
var mde = new EasyMDE({
|
||||
autoDownloadFontAwesome: false,
|
||||
autofocus: true,
|
||||
|
@ -47,8 +46,94 @@ var dom_md = document.getElementById('mt');
|
|||
tabSize: 4,
|
||||
toolbar: tbar
|
||||
});
|
||||
md_changed(mde, true);
|
||||
mde.codemirror.on("change", function () {
|
||||
md_changed(mde);
|
||||
});
|
||||
var loader = document.getElementById('ml');
|
||||
loader.parentNode.removeChild(loader);
|
||||
})();
|
||||
|
||||
zsetTimeout(function () { window.location.reload(true); }, 1000);
|
||||
function md_changed(mde, on_srv) {
|
||||
if (on_srv)
|
||||
window.md_saved = mde.value();
|
||||
|
||||
var md_now = mde.value();
|
||||
var save_btn = document.querySelector('.editor-toolbar button.save');
|
||||
|
||||
if (md_now == window.md_saved)
|
||||
save_btn.classList.add('disabled');
|
||||
else
|
||||
save_btn.classList.remove('disabled');
|
||||
}
|
||||
|
||||
function save(mde) {
|
||||
var save_btn = document.querySelector('.editor-toolbar button.save');
|
||||
if (save_btn.classList.contains('disabled')) {
|
||||
alert('there is nothing to save');
|
||||
return;
|
||||
}
|
||||
var force = save_btn.classList.contains('force-save');
|
||||
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document')) {
|
||||
alert('ok, aborted');
|
||||
return;
|
||||
}
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append("act", "tput");
|
||||
fd.append("lastmod", (force ? -1 : last_modified));
|
||||
fd.append("body", mde.value());
|
||||
|
||||
var url = (document.location + '').split('?')[0] + '?raw';
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.responseType = 'text';
|
||||
xhr.onreadystatechange = save_cb;
|
||||
xhr.btn = save_btn;
|
||||
xhr.mde = mde;
|
||||
xhr.send(fd);
|
||||
}
|
||||
|
||||
function save_cb() {
|
||||
if (this.readyState != XMLHttpRequest.DONE)
|
||||
return;
|
||||
|
||||
if (this.status !== 200) {
|
||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
return;
|
||||
}
|
||||
|
||||
var r;
|
||||
try {
|
||||
r = JSON.parse(this.responseText);
|
||||
}
|
||||
catch {
|
||||
alert('Failed to parse reply from server:\n\n' + this.responseText);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!r.ok) {
|
||||
if (!this.btn.classList.contains('force-save')) {
|
||||
this.btn.classList.add('force-save');
|
||||
var msg = [
|
||||
'This file has been modified since you started editing it!\n',
|
||||
'if you really want to overwrite, press save again.\n',
|
||||
'modified ' + ((r.now - r.lastmod) / 1000) + ' seconds ago,',
|
||||
((r.lastmod - last_modified) / 1000) + ' sec after you opened it\n',
|
||||
last_modified + ' lastmod when you opened it,',
|
||||
r.lastmod + ' lastmod on the server now,',
|
||||
r.now + ' server time now,\n',
|
||||
];
|
||||
alert(msg.join('\n'));
|
||||
}
|
||||
else {
|
||||
alert('Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
last_modified = r.lastmod;
|
||||
this.btn.classList.remove('force-save');
|
||||
alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
||||
md_changed(this.mde, true);
|
||||
}
|
||||
|
|
|
@ -31,6 +31,10 @@
|
|||
{%- if html %}
|
||||
{{ html }}
|
||||
{%- endif %}
|
||||
|
||||
{%- if click %}
|
||||
<script>document.getElementsByTagName("a")[0].click()</script>
|
||||
{%- endif %}
|
||||
</div>
|
||||
|
||||
{%- if redir %}
|
||||
|
|
|
@ -81,15 +81,15 @@ function opclick(ev) {
|
|||
function goto(dest) {
|
||||
var obj = document.querySelectorAll('.opview.act');
|
||||
for (var a = obj.length - 1; a >= 0; a--)
|
||||
obj[a].setAttribute('class', 'opview');
|
||||
obj[a].classList.remove('act');
|
||||
|
||||
var obj = document.querySelectorAll('#ops>a');
|
||||
for (var a = obj.length - 1; a >= 0; a--)
|
||||
obj[a].setAttribute('class', '');
|
||||
obj[a].classList.remove('act');
|
||||
|
||||
if (dest) {
|
||||
document.querySelector('#ops>a[data-dest=' + dest + ']').setAttribute('class', 'act');
|
||||
document.getElementById('op_' + dest).setAttribute('class', 'opview act');
|
||||
var dom_obj = document.getElementById('op_' + dest).classList.add('act');
|
||||
document.querySelector('#ops>a[data-dest=' + dest + ']').classList.add('act');
|
||||
|
||||
var fn = window['goto_' + dest];
|
||||
if (fn)
|
||||
|
|
|
@ -55,8 +55,7 @@
|
|||
font-size: 1.05em;
|
||||
}
|
||||
#ops,
|
||||
#op_bup,
|
||||
#op_mkdir {
|
||||
.opbox {
|
||||
border: 1px solid #3a3a3a;
|
||||
box-shadow: 0 0 1em #222 inset;
|
||||
}
|
||||
|
@ -68,8 +67,7 @@
|
|||
border-radius: .3em;
|
||||
border-width: .15em 0;
|
||||
}
|
||||
#op_bup,
|
||||
#op_mkdir {
|
||||
.opbox {
|
||||
background: #2d2d2d;
|
||||
margin: 1.5em 0 0 0;
|
||||
padding: .5em;
|
||||
|
@ -77,11 +75,10 @@
|
|||
border-width: .15em .3em .3em 0;
|
||||
max-width: 40em;
|
||||
}
|
||||
#op_mkdir input,
|
||||
#op_bup input {
|
||||
.opbox input {
|
||||
margin: .5em;
|
||||
}
|
||||
#op_mkdir input[type=text] {
|
||||
.opbox input[type=text] {
|
||||
color: #fff;
|
||||
background: #383838;
|
||||
border: none;
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
href="#" data-dest="">---</a><i></i><a
|
||||
href="#" data-dest="up2k">up2k</a><i></i><a
|
||||
href="#" data-dest="bup">bup</a><i></i><a
|
||||
href="#" data-dest="mkdir">mkdir</a></div>
|
||||
href="#" data-dest="mkdir">mkdir</a><i></i><a
|
||||
href="#" data-dest="new_md">new.md</a></div>
|
||||
|
||||
<div id="op_bup" class="opview act">
|
||||
<div id="op_bup" class="opview opbox act">
|
||||
<div id="u2err"></div>
|
||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="/{{ vdir }}">
|
||||
<input type="hidden" name="act" value="bput" />
|
||||
|
@ -13,7 +14,7 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div id="op_mkdir" class="opview act">
|
||||
<div id="op_mkdir" class="opview opbox act">
|
||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="/{{ vdir }}">
|
||||
<input type="hidden" name="act" value="mkdir" />
|
||||
<input type="text" name="name" size="30">
|
||||
|
@ -21,6 +22,14 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div id="op_new_md" class="opview opbox act">
|
||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="/{{ vdir }}">
|
||||
<input type="hidden" name="act" value="new_md" />
|
||||
<input type="text" name="name" size="30">
|
||||
<input type="submit" value="create doc">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="op_up2k" class="opview">
|
||||
<form id="u2form" method="post" enctype="multipart/form-data" onsubmit="return false;"></form>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
diff -NarU2 easymde-mod1/src/js/easymde.js easymde-edit/src/js/easymde.js
|
||||
--- easymde-mod1/src/js/easymde.js 2020-05-01 14:34:19.878774400 +0200
|
||||
+++ easymde-edit/src/js/easymde.js 2020-05-01 19:26:56.843140600 +0200
|
||||
+++ easymde-edit/src/js/easymde.js 2020-05-01 21:24:44.142611200 +0200
|
||||
@@ -2189,4 +2189,5 @@
|
||||
};
|
||||
|
||||
|
@ -63,8 +63,8 @@ diff -NarU2 easymde-mod1/src/js/easymde.js easymde-edit/src/js/easymde.js
|
|||
+ var pre2 = get_pre_line(pre1[0] + 1, true) ||
|
||||
+ [cm.lineCount(), null, preview.scrollHeight];
|
||||
+
|
||||
+ console.log('code-center %d, frac %.2f, pre [%d,%d] [%d,%d]',
|
||||
+ md_center_n, md_frac, pre1[0], pre1[2], pre2[0], pre2[2]);
|
||||
+ //console.log('code-center %d, frac %.2f, pre [%d,%d] [%d,%d]',
|
||||
+ // md_center_n, md_frac, pre1[0], pre1[2], pre2[0], pre2[2]);
|
||||
+
|
||||
+ // [0] is the markdown line which matches that preview y-pos
|
||||
+ // and since not all preview lines are tagged with a line-number
|
||||
|
|
Loading…
Reference in a new issue