Created
July 27, 2018 03:18
-
-
Save korc/30232df2f303ee8e2436a039a65e7acc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Encrypt and Share</title> | |
<script src="https://unpkg.com/ace-builds@1.3.3/src-noconflict/ace.js" charset="utf-8"></script> | |
<script src="https://unpkg.com/ace-builds@1.3.3/src-noconflict/mode-markdown.js" charset="utf-8"></script> | |
<script src="https://unpkg.com/qrcode@1.2.0/build/qrcode.js" charset="utf-8"></script> | |
<script> | |
"use strict"; | |
async function keyToId(jwk) { | |
var key = await crypto.subtle.importKey("jwk", jwk, { name: "RSA-OAEP", hash: "SHA-256" }, true, ["encrypt"]); | |
var digest = await crypto.subtle.digest('sha-256', await crypto.subtle.exportKey('spki', key)); | |
return new Uint32Array(digest).reduce((a, b) => a + b.toString(32), "").slice(0, 16); | |
} | |
async function addKeyToList(rcptList, jwk, name) { | |
var keyId = await keyToId(jwk); | |
var opt = document.createElement('option'); | |
opt.value = JSON.stringify(jwk); | |
opt.appendChild(document.createTextNode(name + ': ' + keyId)); | |
opt.setAttribute('data-key-id', keyId); | |
if (rcptList !== null) | |
rcptList.appendChild(opt); | |
return opt; | |
} | |
async function updateMyKeyId(keyInput, rcptList, jwk) { | |
var opt = await addKeyToList(rcptList, jwk, '(me)'); | |
keyInput.value = opt.getAttribute('data-key-id'); | |
keyInput.setAttribute('data-jwk', opt.value); | |
return keyInput.value; | |
} | |
function Editor(textarea) { | |
if (typeof (textarea) === "string") | |
textarea = document.getElementById(textarea); | |
this.textarea = textarea; | |
this.init(); | |
} | |
Editor.prototype = { | |
init: function () { | |
var self = this; | |
var isiPhone = navigator.userAgent.indexOf("iPhone") !== -1; | |
if (!isiPhone && window.ace) { | |
var ui = this.ui = document.createElement('div'); | |
ui.classList.add('ace-editor'); | |
this.textarea.parentNode.insertBefore(this.ui, this.textarea); | |
this.ace = ace.edit(this.ui); | |
var aceSession = this.ace.getSession(); | |
aceSession.setValue(this.textarea.value); | |
aceSession.setOptions({ | |
mode: "ace/mode/markdown", | |
tabSize: 2 | |
}); | |
aceSession.setUseWrapMode(true); | |
this.textarea.style.display = "none"; | |
aceSession.on('change', function (ev, session) { | |
self.textarea.value = session.getValue(); | |
self.isModified = true; | |
}); | |
} else { | |
this.ui = this.textarea; | |
this.ui.addEventListener('change', function () { | |
self.isModified = true; | |
}); | |
} | |
}, | |
get value() { | |
return this.textarea.value; | |
}, | |
set value(value) { | |
this.textarea.value = value; | |
if (this.ace) { | |
this.ace.getSession().setValue(value); | |
} | |
this.isModified = true; | |
}, | |
get isModified() { | |
return this.ui.classList.contains('edit-modified'); | |
}, | |
set isModified(value) { | |
if (value) | |
this.ui.classList.add('edit-modified'); | |
else | |
this.ui.classList.remove('edit-modified'); | |
} | |
} | |
window.addEventListener('load', async function () { | |
var editor = new Editor('plaintext-md'); | |
var uploadBtn = document.getElementById('upload-btn'); | |
var encKeyData; | |
var genKeyBtn = document.getElementById('generate-key-btn'); | |
var myKeyInput = document.getElementById('my-key-id'); | |
var uploadIdInput = document.getElementById('upload-id'); | |
var uploadPathInput = document.getElementById('upload-path'); | |
var downloadBtn = document.getElementById('download-btn'); | |
var rcptList = document.getElementById('rcpt'); | |
var copyMyKeyBtn = document.getElementById('copy-key-btn'); | |
var showMyKeyBtn = document.getElementById('show-key-btn'); | |
var viewKeyArea = document.getElementById('view-key-area'); | |
var pasteRcptKeyBtn = document.getElementById('paste-rcpt-btn'); | |
var qrCodeCanvas = null; | |
if (window.QRCode) { | |
showMyKeyBtn.addEventListener('click', function () { | |
if (qrCodeCanvas === null) { | |
qrCodeCanvas = document.createElement('canvas'); | |
viewKeyArea.appendChild(qrCodeCanvas); | |
QRCode.toCanvas(qrCodeCanvas, "n=" + myKey.public.n); | |
} else { | |
qrCodeCanvas.parentNode.removeChild(qrCodeCanvas); | |
qrCodeCanvas = null; | |
} | |
}); | |
} else | |
showMyKeyBtn.style.display = "none"; | |
document.getElementById('data-new-btn').addEventListener('click', function () { | |
if (editor.isModified && !confirm("Document modified, are you sure?")) | |
return; | |
document.location.replace('#'); | |
document.location.reload(); | |
}); | |
window.onbeforeunload = function (ev) { | |
if (editor.isModified) | |
return "Editor changes not saved"; | |
}; | |
async function addRecipient(data) { | |
if (data.slice(0, 2) === 'n=') { | |
var jwk = { alg: "RSA-OAEP-256", e: "AQAB", ext: true, key_ops: ["encrypt"], kty: "RSA", n: data.slice(2) }; | |
var keyId = await keyToId(jwk); | |
var name = prompt("Name for '" + keyId + "': "); | |
if (name !== null) { | |
await addKeyToList(rcptList, jwk, name); | |
localStorage["rcpt-list"] = JSON.stringify(Array.prototype.map.call(rcptList.options, function (opt) { | |
var jwk = JSON.parse(opt.value); | |
if (jwk.n === myKey.public.n) | |
return null; | |
return { jwk: jwk, name: opt.label.slice(0, opt.label.indexOf(':')) }; | |
}).filter((r) => r !== null)); | |
} | |
} else { | |
alert('Could not find recipient key in clipboard. Please make sure it is a long string starting with "n="'); | |
throw "bad recipient data"; | |
} | |
} | |
var pasteKeyInput = null; | |
function pasteKeyComplete() { | |
addRecipient(pasteKeyInput.value).then(function () { | |
pasteKeyInput.parentNode.removeChild(pasteKeyInput); | |
pasteKeyInput = null; | |
}); | |
} | |
pasteRcptKeyBtn.addEventListener('click', async function () { | |
if (pasteKeyInput !== null) { | |
if (pasteKeyInput.value === '') { | |
pasteKeyInput.parentNode.removeChild(pasteKeyInput); | |
pasteKeyInput = null; | |
} else { | |
pasteKeyComplete(); | |
} | |
return; | |
} else { | |
pasteKeyInput = document.createElement('input'); | |
pasteKeyInput.placeholder = "n=xxxxx (paste here)"; | |
this.parentNode.insertBefore(pasteKeyInput, this); | |
pasteKeyInput.focus(); | |
if (document.execCommand('paste')) { | |
pasteKeyComplete(); | |
return; | |
} | |
} | |
pasteKeyInput.addEventListener('blur', function () { | |
if (this.value === '') | |
return; | |
pasteKeyComplete(); | |
}); | |
}); | |
var myKey, myKeyId; | |
if ("my-key" in localStorage) { | |
myKey = JSON.parse(localStorage["my-key"]); | |
genKeyBtn.style.display = "none"; | |
copyMyKeyBtn.style.display = ""; | |
myKeyId = await updateMyKeyId(myKeyInput, rcptList, myKey.public); | |
} else { | |
myKeyInput.style.display = "none"; | |
copyMyKeyBtn.style.display = "none"; | |
genKeyBtn.addEventListener('click', async function () { | |
var keyPair = await crypto.subtle.generateKey({ | |
name: 'RSA-OAEP', | |
hash: { name: 'SHA-256' }, | |
modulusLength: 4096, | |
publicExponent: new Uint8Array([1, 0, 1]) | |
}, | |
true, ['encrypt', 'decrypt']); | |
myKey = { | |
private: await crypto.subtle.exportKey('jwk', keyPair.privateKey), | |
public: await crypto.subtle.exportKey('jwk', keyPair.publicKey) | |
}; | |
localStorage["my-key"] = JSON.stringify(myKey); | |
myKeyId = await updateMyKeyId(myKeyInput, rcptList, myKey.public); | |
myKeyInput.style.display = ""; | |
copyMyKeyBtn.style.display = ""; | |
genKeyBtn.style.display = "none"; | |
}); | |
} | |
if ("rcpt-list" in localStorage) { | |
JSON.parse(localStorage["rcpt-list"]).forEach(function (rcpt) { | |
addKeyToList(rcptList, rcpt.jwk, rcpt.name); | |
}); | |
} | |
copyMyKeyBtn.addEventListener('click', function () { | |
var i = document.createElement('input'); | |
this.parentNode.insertBefore(i, this); | |
i.value = "n=" + myKey.public.n; | |
i.select(); | |
if (!document.execCommand('copy') || confirm('Key info copied to clipboard.\nDo you want to send a mail?')) { | |
document.location.href = "mailto:?subject=My recipient key&body=" + encodeURIComponent("Hi,\n\nPlease add me as recipient on " + document.location.href + " with this key:\n\n" + i.value + "\n\nKey ID: " + myKeyId + "\n\nThanks!"); | |
}; | |
this.parentNode.removeChild(i); | |
}) | |
rcptList.addEventListener("change", function () { | |
uploadBtn.disabled = !encKeyData && this.selectedOptions.length === 0; | |
}); | |
uploadIdInput.addEventListener('change', function () { | |
downloadBtn.disabled = !myKey || (this.value === ""); | |
}); | |
uploadIdInput.addEventListener('keyup', function () { | |
downloadBtn.disabled = !myKey || (this.value === ""); | |
}); | |
function startDownload() { | |
if (editor.isModified && !confirm("Data modified, are you sure to discard changes?")) | |
return; | |
var uploadPath = uploadPathInput.value; | |
var uploadId = uploadIdInput.value; | |
var xhr = new XMLHttpRequest(); | |
xhr.open('GET', uploadPath + uploadId + '.enc'); | |
xhr.responseType = 'arraybuffer'; | |
xhr.addEventListener('load', function () { | |
if (this.status !== 200) { | |
alert('Cannot find data: ' + uploadId); | |
return; | |
} | |
history.replaceState({}, '', '#' + uploadId); | |
var encData = this.response; | |
var xhr2 = new XMLHttpRequest(); | |
xhr2.open('GET', uploadPath + uploadId + '.key.' + myKeyId); | |
xhr2.responseType = 'arraybuffer'; | |
xhr2.addEventListener('load', function () { | |
if (this.status !== 200) { | |
alert('Your key is not included in recipients list.\n\nPlease request from data owner to be added.\n' + (myKeyId ? ('\nYour key ID: ' + myKeyId) : 'You also need to generate and send your key first.') + '\n(Use key info on top-right corner)'); | |
return; | |
} | |
var wrappedEncKey = this.response; | |
var xhr3 = new XMLHttpRequest(); | |
xhr3.open('GET', uploadPath + uploadId + '.iv'); | |
xhr3.responseType = 'arraybuffer'; | |
xhr3.addEventListener('load', async function () { | |
if (this.status !== 200) { | |
alert('Uh-oh.. IV lost?'); | |
return; | |
} | |
var ivData = this.response; | |
var alg = { name: 'RSA-OAEP', hash: 'SHA-256' }; | |
var rcptKey = await crypto.subtle.importKey('jwk', myKey.private, alg, false, ['decrypt']); | |
encKeyData = await crypto.subtle.decrypt(alg, rcptKey, wrappedEncKey); | |
alg = { name: 'AES-GCM', iv: ivData }; | |
var dataKey = await crypto.subtle.importKey('raw', encKeyData, alg, false, ['decrypt']); | |
var data = await crypto.subtle.decrypt(alg, dataKey, encData); | |
editor.value = new TextDecoder().decode(data); | |
editor.isModified = false; | |
uploadBtn.disabled = false; | |
}); | |
xhr3.send(null); | |
}); | |
xhr2.send(null); | |
}); | |
xhr.send(null); | |
} | |
downloadBtn.addEventListener('click', startDownload); | |
uploadBtn.addEventListener('click', async function () { | |
var uploadPath = uploadPathInput.value; | |
var uploadId = uploadIdInput.value; | |
if (uploadId === '') { | |
uploadIdInput.value = uploadId = crypto.getRandomValues(new Uint8Array(8)).reduce((a, b) => a + (b < 0x10 ? "0" : "") + b.toString(16), ""); | |
history.replaceState({}, '', '#' + uploadId); | |
uploadBtn.value = 'Update'; | |
} | |
if (!encKeyData) | |
encKeyData = crypto.getRandomValues(new Uint8Array(16)); | |
var iv = crypto.getRandomValues(new Uint8Array(12)); | |
var alg = { name: 'AES-GCM', iv: iv }; | |
var encKey = await crypto.subtle.importKey('raw', encKeyData, alg, false, ['encrypt']); | |
var xhr = new XMLHttpRequest(); | |
xhr.open('PUT', uploadPath + uploadId + '.iv'); | |
xhr.send(iv); | |
xhr = new XMLHttpRequest(); | |
xhr.open('PUT', uploadPath + uploadId + '.enc'); | |
xhr.addEventListener('load', function () { | |
if (this.status >= 200 && this.status < 300) | |
editor.isModified = false; | |
}); | |
xhr.send(await crypto.subtle.encrypt(alg, encKey, new TextEncoder().encode(editor.value))); | |
Array.prototype.forEach.call(rcptList.selectedOptions, async function (opt) { | |
var alg = { name: 'RSA-OAEP', hash: "SHA-256" }; | |
var rcptKey = await crypto.subtle.importKey('jwk', JSON.parse(opt.value), alg, false, ['encrypt']); | |
var rcptKeyId = opt.getAttribute('data-key-id'); | |
var wrappedKeyData = await crypto.subtle.encrypt(alg, rcptKey, encKeyData); | |
xhr = new XMLHttpRequest(); | |
xhr.open('PUT', uploadPath + uploadId + '.key.' + rcptKeyId); | |
xhr.send(wrappedKeyData); | |
}); | |
}); | |
if (document.location.hash.length > 2) { | |
uploadIdInput.value = unescape(document.location.hash.slice(1)); | |
startDownload(); | |
} | |
var uainfo = document.createElement("div"); | |
uainfo.classList.add("dbg"); | |
uainfo.style.cssFloat = "right"; | |
uainfo.appendChild(document.createTextNode(navigator.userAgent)); | |
document.body.appendChild(uainfo); | |
}); | |
</script> | |
<style> | |
.ace-editor { | |
width: 50%; | |
height: 400px; | |
} | |
.dbg { | |
font-family: monospace; | |
font-size: 0.7em; | |
color: gray; | |
} | |
.edit-modified { | |
outline: dashed maroon 2px; | |
} | |
.edit-modified::after { | |
content: " *Unsaved*"; | |
color: gray; | |
font-style: italic; | |
} | |
</style> | |
</head> | |
<body> | |
<div style="float: right"> | |
My ID: | |
<input placeholder="My Key ID" id="my-key-id" disabled> | |
<button id="generate-key-btn">Generate my key</button> | |
<button id="copy-key-btn">🗐</button> | |
<button id="show-key-btn">👁</button> | |
<div id="view-key-area"></div> | |
</div> | |
Data ID: | |
<input id="upload-path" value="/data/" type="hidden" /> | |
<input placeholder="Upload ID" id="upload-id"> | |
<button id="download-btn" disabled>Load</button> | |
<button id="data-new-btn">New</button> | |
<textarea cols="80" rows="40" id="plaintext-md"></textarea> | |
<br/> Recipients: | |
<select id="rcpt" multiple></select> | |
<button id="paste-rcpt-btn">📋</button> | |
<br> | |
<button id="upload-btn" disabled>Save data</button> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<title>file admin</title> | |
<script> | |
"use strict"; | |
function updateDav(input) { | |
document.location.hash = '#' + input.value; | |
} | |
function davNSR(p) { | |
var ns = { D: 'DAV:' }; | |
return ns[p] || null; | |
} | |
function formatTimeInterval(secs, nrOfParts) { | |
if (nrOfParts) | |
nrOfParts *= 2; | |
return [[60, "m"], [60, "h"], [24, "d"], [7, "w"], null] | |
.reduce(function (a, n) { | |
if (typeof (a) === "string") return a; | |
if (!n) return a.slice(0, nrOfParts).join(""); | |
var v = Math.floor(a[0] / n[0]), d = a[0] - v * n[0]; | |
return v ? [v, n[1]].concat(d ? [parseInt(d * 1000) / 1000, a[1]] : [], a.slice(2)) : a.join(""); | |
}, [secs, "s"]); | |
} | |
function formatBytes(bytes) { | |
if (bytes < 1024) return parseInt(bytes) + "b"; | |
if (bytes < 1024 * 1024) return (Math.round(10 * bytes / 1024) / 10) + "kb"; | |
if (bytes < 1024 * 1024 * 1024) return (Math.round(10 * bytes / 1024 / 1024) / 10) + "mb"; | |
return (Math.round(10 * bytes / 1024 / 1024 / 1024) / 10) + "gb"; | |
} | |
function DAVObject(path, properties) { | |
this.path = path; | |
this.properties = properties; | |
if (this.isCollection) | |
this.items = []; | |
} | |
DAVObject.prototype = { | |
_xhrProm: function (xhr, data) { | |
return new Promise(function (resolve, reject) { | |
xhr.onload = function (ev) { | |
if (!(xhr.status >= 200 && xhr.status < 300)) | |
reject(xhr.status + " " + xhr.statusText); | |
resolve(this.response); | |
} | |
xhr.onerror = function (ev) { reject(ev); }; | |
xhr.send(data); | |
}); | |
}, | |
doPropfind: function (depth) { | |
if (depth === undefined) | |
depth = 1; | |
var xhr = new XMLHttpRequest(); | |
xhr.open('PROPFIND', this.path); | |
xhr.setRequestHeader('Depth', '' + depth); | |
xhr.responseType = 'document'; | |
return this._xhrProm(xhr, null); | |
}, | |
doPut: function (data) { | |
var xhr = new XMLHttpRequest(); | |
xhr.open('PUT', this.path); | |
return this._xhrProm(xhr, data); | |
}, | |
doDelete: function () { | |
var xhr = new XMLHttpRequest(); | |
xhr.open('DELETE', this.path); | |
return this._xhrProm(xhr, null); | |
}, | |
parsePropfind: function (xml) { | |
var resIter = xml.evaluate('/D:multistatus/D:response', xml, davNSR, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); | |
var resNode, href, prop, propIter, propVal, properties, propName, rtypeIter, rtype; | |
var firstResult = true; | |
while (resNode = resIter.iterateNext()) { | |
href = xml.evaluate('D:href', resNode, davNSR, XPathResult.STRING_TYPE, null).stringValue; | |
propIter = xml.evaluate('D:propstat/D:prop/*', resNode, davNSR, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); | |
properties = {}; | |
while (prop = propIter.iterateNext()) { | |
if (prop.namespaceURI !== "DAV:") | |
console.log("Warning: property not in 'DAV:' namespace: ", prop.namespaceURI) | |
switch (prop.localName) { | |
case "getlastmodified": case "getcontentlength": case "getetag": case "getcontenttype": case "displayname": | |
propVal = xml.evaluate('text()', prop, davNSR, XPathResult.STRING_TYPE).stringValue; | |
if (!propVal) | |
break; | |
if (prop.localName === "getlastmodified") | |
propVal = new Date(propVal); | |
properties[prop.localName.slice(0, 3) === "get" ? prop.localName.slice(3) : prop.localName] = propVal; | |
break; | |
case "resourcetype": | |
rtypeIter = xml.evaluate("./*", prop, davNSR, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); | |
properties.resourcetype = {}; | |
while (rtype = rtypeIter.iterateNext()) { | |
properties.resourcetype[rtype.localName] = true; | |
} | |
break; | |
case "supportedlock": break; | |
default: | |
console.log("Warning: don't know how to handle: ", prop.localName, prop); | |
properties[prop.localName] = xml.evaluate('text()', prop, davNSR, XPathResult.STRING_TYPE).stringValue; | |
} | |
} | |
if (firstResult) { | |
firstResult = false; | |
this.properties = properties; | |
this.href = href; | |
if (this.isCollection) | |
this.items = []; | |
} else { | |
this.items.push(new DAVObject(href, properties)); | |
} | |
} | |
}, | |
get isCollection() { | |
return this.properties && this.properties.resourcetype && this.properties.resourcetype.collection; | |
} | |
}; | |
function createButton(label, callback) { | |
var btn = document.createElement('button'); | |
if (label) | |
btn.appendChild(document.createTextNode(label)); | |
btn.addEventListener('click', callback); | |
return btn; | |
} | |
function listFolder(container, addr) { | |
var dObj = new DAVObject(addr); | |
while (container.firstChild) container.removeChild(container.firstChild); | |
container.appendChild(document.createTextNode("(refreshing " + addr + ")")); | |
dObj.doPropfind().then(function (xmlResponse) { | |
while (container.firstChild) container.removeChild(container.firstChild); | |
dObj.parsePropfind(xmlResponse); | |
var ul = document.createElement('ul'); | |
container.appendChild(document.createTextNode("[" + (dObj.properties.displayname || dObj.href) + "]")); | |
container.appendChild(createButton('🔄', function () { | |
listFolder(container, addr); | |
})); | |
container.appendChild(createButton('⇪', function () { | |
var ulBtn = this; | |
if (this.nextSibling.tagName === 'INPUT' && this.nextSibling.type === 'file') { | |
this.classList.remove('pressed'); | |
this.parentNode.removeChild(this.nextSibling); | |
return; | |
} | |
this.classList.add('pressed'); | |
var flist = document.createElement('input'); | |
flist.type = 'file'; | |
flist.multiple = true; | |
flist.addEventListener('change', function () { | |
var fileCount = this.files.length; | |
Array.prototype.forEach.call(this.files, function (file) { | |
var uploadSpan = document.createElement('span'); | |
uploadSpan.classList.add('upload-status'); | |
uploadSpan.appendChild(document.createTextNode(file.name)); | |
flist.parentNode.insertBefore(uploadSpan, flist.nextSibling); | |
var newFile = new DAVObject((dObj.href + (dObj.href.slice(-1) === '/' ? '' : '/')) + file.name) | |
newFile.doPut(file).then(function () { | |
fileCount--; | |
uploadSpan.classList.add('complete'); | |
if (fileCount === 0) listFolder(container, addr); | |
}).catch(function (err) { | |
uploadSpan.classList.add('error'); | |
uploadSpan.setAttribute('data-error', err); | |
}); | |
}); | |
this.parentNode.removeChild(this); | |
ulBtn.classList.remove('pressed'); | |
}); | |
this.parentNode.insertBefore(flist, this.nextSibling); | |
})); | |
if (dObj.properties.lastmodified) | |
container.appendChild(document.createTextNode(" (mod: " + formatTimeInterval((new Date() - dObj.properties.lastmodified) / 1000, 2) + " ago)")); | |
container.appendChild(ul); | |
dObj.items.forEach(function (item) { | |
var li = document.createElement('li'); | |
var a; | |
if (item.isCollection) { | |
a = createButton(null, function () { | |
listFolder(li, item.path); | |
}); | |
} else { | |
a = document.createElement('a'); | |
a.href = item.path; | |
} | |
a.appendChild(document.createTextNode(item.properties.displayname || item.href)); | |
li.appendChild(a); | |
if (!item.isCollection) { | |
li.appendChild(createButton('🗑️', function () { | |
if (!confirm("Do you want to delete '" + item.path + "'?")) | |
return; | |
item.doDelete().then(function () { | |
listFolder(container, dObj.path); | |
}); | |
})); | |
} | |
if ("contentlength" in item.properties) | |
li.appendChild(document.createTextNode(" (size: " + formatBytes(item.properties.contentlength) + ")")); | |
if ("contenttype" in item.properties) | |
li.appendChild(document.createTextNode(" (type: " + item.properties.contenttype + ")")); | |
if (item.properties.lastmodified) { | |
var diff = new Date() - new Date(item.properties.lastmodified); | |
li.appendChild(document.createTextNode(" (mod: " + formatTimeInterval(diff / 1000, 2) + " ago)")); | |
} | |
ul.appendChild(li); | |
}); | |
}).catch(function (error) { | |
var err = document.createElement('span'); | |
err.classList.add('error'); | |
err.appendChild(document.createTextNode("" + error)); | |
container.appendChild(err); | |
}); | |
} | |
function doList() { | |
listFolder(document.getElementById('results'), document.getElementById('webdav-input').value); | |
} | |
</script> | |
<style> | |
span.error { | |
color: red; | |
background-color: bisque; | |
border: 2px dashed red; | |
} | |
span.upload-status.complete { | |
outline: 1px solid green; | |
} | |
span.upload-status.error::after { | |
content: " (" attr(data-error) ")"; | |
} | |
button.pressed { | |
border-style: inset; | |
} | |
</style> | |
</head> | |
<body> | |
<input onchange="updateDav(this)" placeholder="https://webdav.site/addr" id="webdav-input"> | |
<button onclick="doList()">Open</button> | |
<hr> | |
<div id="results"></div> | |
<script> | |
"use strict"; | |
if (document.location.hash !== '') { | |
document.getElementById('webdav-input').value = unescape(document.location.hash.substr(1, )); | |
doList(); | |
} | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
: "${ONELINE_WEBSRV:=websrv}" | |
test -x "$(which "$ONELINE_WEBSRV")" || { | |
echo "Cannot find \$ONELINE_WEBSRV='$ONELINE_WEBSRV'" >&2 | |
exit 1 | |
} | |
set -e -x | |
mkdir -p dav | |
exec $ONELINE_WEBSRV -listen 127.0.0.1:3000 -map /=file: -map /data=webdav:dav |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment