diff --git a/Sunfish/ShareWeb/$sunfish/$index.html b/Sunfish/ShareWeb/$sunfish/$index.html index d71dfe4..8da28f7 100644 --- a/Sunfish/ShareWeb/$sunfish/$index.html +++ b/Sunfish/ShareWeb/$sunfish/$index.html @@ -30,7 +30,7 @@
- +
{Name}
{Description}
{@Actions}
{%Actions:action}
{/} diff --git a/Sunfish/ShareWeb/$sunfish/style.css b/Sunfish/ShareWeb/$sunfish/style.css index b57d43e..2c4fcc9 100644 --- a/Sunfish/ShareWeb/$sunfish/style.css +++ b/Sunfish/ShareWeb/$sunfish/style.css @@ -72,6 +72,24 @@ vertical-align: bottom; } +#main-toolbar { + position: absolute; + right: 4rem; + top: 0.8rem; +} + +#main-toolbar i.material-icons-round { + font-size: 26px; + cursor: pointer; + color: dimgrey; +} + +#main-toolbar i.material-icons-round:hover { + font-size: 26px; + cursor: pointer; + color: #818181; +} + #wrapper { margin: 0 auto; padding: 0.5rem; @@ -136,7 +154,7 @@ margin-left: 0.3rem; } -.test{ +.test { background-color: #4f6e05; } @@ -240,4 +258,93 @@ input:hover { background-color: #cecece; +} + +.upload-drop { + background-color: rgba(0, 0, 0, 0.75); +} + +.upload-drop-icon { + position: absolute; + top: 5%; + height: 90%; + left: 25%; + width: 50%; + bottom: 25%; + border: 10px dashed #c1c1c1; + border-radius: 2rem; + vertical-align: middle; + overflow: hidden; +} + +.upload-drop-icon>i.material-icons-round { + width: 100%; + color: white; + height: 100%; + vertical-align: middle; + font-size: 8rem; + margin-top: 35%; + text-align: center; +} + +.upload-drop-icon>i.material-icons-round::before { + content: "Drop here to upload"; + position: absolute; + top: 58%; + left: 0; + right: 0; + font-family: 'Open Sans'; + font-size: 1rem; +} + +.upload-list { + height: calc( 100% - 4rem); + overflow-y: auto; + padding: 0 1rem; + margin: 1rem 0; +} + +.upload-progress-box { + height: 2rem; + background: #383838; + position: relative; +} + +.upload-progress { + position: absolute; + top: 0; + left: 0; + bottom: 0; + background: #a6e1ff; +} + +.upload-item { + font-family: 'Open Sans'; + color: white; + margin-bottom: 1rem; +} + +.upload-item-progress-box { + height: 1rem; + background: #383838; + border-radius: 0.5rem; + margin-top: 0.2rem; + border: 1px solid black; + position: relative; + overflow: hidden; +} + +.upload-item-progress { + position: absolute; + top: 0; + left: 0; + bottom: 0; + background: #a6e1ff url(/$sunfish/logo.png) 100% 0.05rem/0.8rem 0.8rem no-repeat; +} + +.upload-close { + margin-top: 0.15rem; + background: #6d6d6d; + color: white; + margin-left: calc( 100% - 5rem); } \ No newline at end of file diff --git a/Sunfish/ShareWeb/$sunfish/sunfish-directory.js b/Sunfish/ShareWeb/$sunfish/sunfish-directory.js index 4e4ef7f..44fca25 100644 --- a/Sunfish/ShareWeb/$sunfish/sunfish-directory.js +++ b/Sunfish/ShareWeb/$sunfish/sunfish-directory.js @@ -11,6 +11,7 @@ } sunfish.deleteFile = function (sender) { + //TODO: use the DELTE http method var ask; var item = getItem(sender); var isfolder = item.className.indexOf("directory") >= 0; @@ -59,4 +60,233 @@ }); }); } + + sunfish.openFile = function (sender) { + var item = getItem(sender); + var file = item.querySelector("div.item-name").innerText; + sunfish.ajax(document.location.href + file + "?action=open", { + ok: function (result) { + if (result != "OK") + sunfish.error("Error opening file in server", function () { + document.location.reload(); + }); + } + }); + } + + sunfish.newFolder = function (sender) { + sunfish.askString('Folder name:', "", function (name) { + if (!name) + return; + sunfish.ajax(document.location.href + "?action=createFolder&name=" + name, { + ok: function (result) { + if (result != "OK") + sunfish.error("Error creading folder", function () { + document.location.reload(); + }); + } + }); + }); + } + + sunfish.newFile = function (sender) { + sunfish.askString('File name:', "", function (name) { + if (!name) + return; + sunfish.ajax(document.location.href + "?action=createFolder&name=" + name, { + ok: function (result) { + if (result != "OK") + sunfish.error("Error creading file", function () { + document.location.reload(); + }); + } + }); + }); + } + + sunfish.uploadFile = function (sender) { + var efile = document.createElement("input"); + efile.type = "file"; + efile.onchange = () => { + var fileList = efile.files; + for (var i = 0; i < fileList.length; i++) + upl.upload(fileList[i]); + } + efile.click(); + } + + // UPLOAD task + var upl; + function Uploader() { + var pb, elist, uploads = []; + var edrop = sunfish.build({ + $: "div", className: "popup-wall upload-drop", _: [ + { + $: "div", className: "upload-drop-icon", _: [ + { $: "i", className: "material-icons-round", _: ["upload"] } + ] + } + ] + }); + var ctt = edrop.querySelector("div.upload-drop-icon"); + + function show() { + document.body.appendChild(edrop); + var cbutton; + ctt.innerHTML = ""; + elist = sunfish.build({ $: "div", className: "upload-list" }, ctt); + elist.addItem = function (file, name, to) { + if (!pb.updater) { + pb.updater = function () { + var tpos = 0, tmax = 0; + uploads.forEach(u => { + tpos += u.pos; + tmax += u.length; + }) + if (tmax == 0) + tmax = 100; + pb.style.width = (tpos / tmax * 100) + "%"; + if (tpos < tmax) { + setTimeout(pb.updater, 100); + cbutton.style.display = "none"; + } else { + delete pb.updater; + cbutton.style.display = ""; + } + } + setTimeout(pb.updater, 100); + } + var item = sunfish.build({ + $: "div", className: "upload-item", _: [{ $: "div", _: [to] }, + { $: "div", className: "upload-item-progress-box", _: [{ $: "div", className: "upload-item-progress" }] }] + }, elist); + var itemProgress = item.querySelector(".upload-item-progress"); + var me; + uploads.push(me = { + file: file, + name: name, + to: to, + progress: itemProgress, + length: file.size, + pos: 0, + eof: false + }); + function updateProgress() { + if (me.length <= 0) + return; + var pos = me.pos; + if (pos > me.length) + pos = me.length; + itemProgress.style.width = (pos / me.length) * 100 + "%"; + + } + function ko(info) { + itemProgress.style.width = "150%"; + itemProgress.style.backgroundColor = "tomato"; + if (info) + console.error(info); + } + function step() { + var length = Math.min(1024 * 128, me.length - me.pos); + var blob = file.slice(me.pos, me.pos + length); + if (blob.length == 0) + return ko("Nothing to send"); + sunfish.ajax(document.location.href + to, { + method: "PUT", + data: blob, + headers: { + "X-Sunfish-Offset": me.pos, + "X-Sunfish-Length": blob.size + }, + ok: function (result) { + if (result != "OK") + ko("Upload is "+result); + else { + me.pos += blob.size; + updateProgress(); + if (me.pos < me.length) + step(); + else { + itemProgress.style.backgroundColor = "#46ad33"; + me.eof = true; + } + } + }, + ko: ko + }); + } + step(); + } + var pbb = sunfish.build({ $: "div", className: "upload-progress-box" }, ctt); + pb = sunfish.build({ + $: "div", className: "upload-progress", _: [ + { + $: "button", className: "upload-close", style: { display: "none" }, _: ["Close"], onclick: function () { + document.body.removeChild(edrop); + uploads = []; + } + } + ] + }, pbb); + cbutton = pb.querySelector(".upload-close"); + } + + document.body.addEventListener("drop", function (ev) { + function processWKItem(item, path) { + path = path || ""; + if (item.isFile) { + item.file(function (file) { + upload(file, path + file.name); + }); + } else if (item.isDirectory) { + // Get folder contents + var dirReader = item.createReader(); + dirReader.readEntries(function (entries) { + for (var i = 0; i < entries.length; i++) { + processWKItem(entries[i], path + item.name + "/"); + } + }); + } + } + ev.preventDefault(); + if (ev.dataTransfer.items) { + var items = ev.dataTransfer.items; + for (var i = 0; i < items.length; i++) { + var item = items[i]; + if (item.kind === 'file') + if (item.webkitGetAsEntry) + processWKItem(item.webkitGetAsEntry()); + else + upload(item.getAsFile()); + } + } else { + for (var i = 0; i < ev.dataTransfer.files.length; i++) + upload(ev.dataTransfer.files[i].getAsFile()); + } + }); + + document.body.addEventListener("dragover", function (ev) { + ev.preventDefault(); + document.body.appendChild(edrop); + }); + document.body.addEventListener("dragleave", function (ev) { + ev.preventDefault(); + //if (uploads.length == 0) + //document.body.removeChild(edrop); //Some strange behavior here Chrome thing? + }); + + function upload(file, to) { + if (!elist) + show(); + to = to || file.name; + elist.addItem(file, file.name, to); + } + + this.upload = upload; + } + + window.addEventListener("load", function () { + upl = new Uploader(); + }); + })(); \ No newline at end of file diff --git a/Sunfish/ShareWeb/$sunfish/sunfish.js b/Sunfish/ShareWeb/$sunfish/sunfish.js index 87838fb..46684d5 100644 --- a/Sunfish/ShareWeb/$sunfish/sunfish.js +++ b/Sunfish/ShareWeb/$sunfish/sunfish.js @@ -27,8 +27,8 @@ delete def._; for (var i in def) { if (i == "style") - for (var sn in def) - el.style[sn] = def[sn]; + for (var sn in def.style) + el.style[sn] = def.style[sn]; else el[i] = def[i]; } @@ -43,24 +43,48 @@ return el; } - function ask(question, bt1, bt2, bt3, bt4, bt5, iserror) { + function dialog(content, buttons, classes) { + var btdef = []; + for (var i in buttons) { + var bt = buttons[i]; + if (bt.$) + btdef.push(bt); + else + btdef.push({ + $: "button", + className: bt.class, + _: bt.b, + onclick: bt.do + }); + } + var wall = build({ $: "div", className: "popup-wall" }, document.body); var def = { - $: "div", className: iserror ? "popup error" : "popup", _: [ - { $: "div", className: "body", _: [question] }, - { $: "div", className: "buttons", _: [] } + $: "div", className: "popup" + (classes ? " " + classes : ""), _: [ + { $: "div", className: "body", _: content }, + { $: "div", className: "buttons", _: btdef } ] }; + var dialog = build(def, wall); + dialog.close = function () { + document.body.removeChild(wall); + } + return dialog; + } + + function ask(question, bt1, bt2, bt3, bt4, bt5, iserror) { + var buttons = []; var el; function addbt(bt) { if (bt) { if (bt.go) bt.do = function () { document.location = bt.go }; - def._[1]._.push({ - $: "button", className: bt.class, _: bt.b, - onclick: function () { + buttons.push({ + class: bt.class, + b: bt.b, + do: function () { bt.do && bt.do(); - document.body.removeChild(wall); + el.close(); } }); } @@ -70,7 +94,7 @@ addbt(bt3); addbt(bt4); addbt(bt5); - return el = build(def, wall); + return el = dialog([question], buttons, iserror ? "error" : null); } function askString(text, def, cb) { @@ -123,11 +147,30 @@ ctrl.ok && ctrl.ok(xhr.responseText, xhr); }; xhr.open(ctrl.method || 'GET', url); - xhr.send(); + if (ctrl.headers) + for (var k in ctrl.headers) + xhr.setRequestHeader(k, ctrl.headers[k]); + if (ctrl.binary) + xhr.sendAsBinary(ctrl.binary, "binary/octet"); + else + xhr.send(ctrl.data); } + if (!XMLHttpRequest.prototype.sendAsBinary) + XMLHttpRequest.prototype.sendAsBinary = function (datastr, contentType) { + var bb = new BlobBuilder(); + var len = datastr.length; + var data = new Uint8Array(len); + for (var i = 0; i < len; i++) { + data[i] = datastr.charCodeAt(i); + } + bb.append(data.buffer); + this.send(bb.getBlob(contentType)); + } + this.init = init; this.build = build; + this.dialog = dialog; this.ask = ask; this.askString = askString; this.say = say; diff --git a/Sunfish/Sunfish/Extensions.cs b/Sunfish/Sunfish/Extensions.cs index ed3592b..f4662c0 100644 --- a/Sunfish/Sunfish/Extensions.cs +++ b/Sunfish/Sunfish/Extensions.cs @@ -12,10 +12,10 @@ MessageBox.Show(ex.Message, ex.GetType().Name, MessageBoxButtons.OK, MessageBoxIcon.Error); } - public static void TransferFrom(this Stream s,Stream from) + public static void TransferFrom(this Stream s, Stream from) { - byte[] buf = new byte[10240];// 10Kb - int readed=buf.Length; + byte[] buf = new byte[524288];// 512Kb + int readed = buf.Length; while (readed == buf.Length) { readed = from.Read(buf, 0, buf.Length); @@ -23,7 +23,21 @@ } } - public static T GetValue(this Dictionary dict,K key, T def) + public static void TransferFrom(this Stream s, Stream from, long length) + { + byte[] buf = new byte[Math.Min(524288, length)];// 512Kb + while (length > 0) + { + int toRead = (int)Math.Min(buf.Length, length); + int readed = from.Read(buf, 0, toRead); + if (readed != toRead) + throw new IOException("Unexpected EOF"); + s.Write(buf, 0, readed); + length -= readed; + } + } + + public static T GetValue(this Dictionary dict, K key, T def) { T value; if (dict.TryGetValue(key, out value)) diff --git a/Sunfish/Sunfish/Middleware/VFS.cs b/Sunfish/Sunfish/Middleware/VFS.cs index ede52e4..ff5489b 100644 --- a/Sunfish/Sunfish/Middleware/VFS.cs +++ b/Sunfish/Sunfish/Middleware/VFS.cs @@ -71,6 +71,29 @@ return folder.GetItem(path); } + public VFSItem Create(string path) + { + bool asDir = path.EndsWith("/"); + if (asDir) + path = path.Substring(0, path.Length - 1); + int sep = path.LastIndexOf('/'); + string inner = path.Substring(sep + 1); + path = path.Substring(0, sep); + VFSItem dir = GetItem(path); + while (dir == null) + { + sep = path.LastIndexOf('/'); + if (sep < 0) + return null; + inner = path.Substring(sep + 1) + '/' + inner; + path = path.Substring(0, sep); + dir = GetItem(path); + } + if (!dir.Directory) + return null; + return dir.Create(inner, asDir); + } + public string[] ListFiles(string path) { VFSFolder folder = LocateFolder(ref path); diff --git a/Sunfish/Sunfish/Middleware/VFSFolder.cs b/Sunfish/Sunfish/Middleware/VFSFolder.cs index 89e3417..ae599d9 100644 --- a/Sunfish/Sunfish/Middleware/VFSFolder.cs +++ b/Sunfish/Sunfish/Middleware/VFSFolder.cs @@ -16,6 +16,7 @@ public abstract string[] ListDirectories(string path); public abstract bool DeleteFile(string path); public abstract bool DeleteFolder(string path); - public abstract bool Rename(string from, string to); + public abstract bool Rename(string from, string to); + public abstract VFSItem Create(string path,bool asFolder); } } diff --git a/Sunfish/Sunfish/Middleware/VFSFolderFileSystem.cs b/Sunfish/Sunfish/Middleware/VFSFolderFileSystem.cs index b30297d..183e88e 100644 --- a/Sunfish/Sunfish/Middleware/VFSFolderFileSystem.cs +++ b/Sunfish/Sunfish/Middleware/VFSFolderFileSystem.cs @@ -33,7 +33,13 @@ public override Stream OpenWrite(string path) { - throw new NotImplementedException(); + path = Path.Combine(basePath, path); + try + { + return File.OpenWrite(path); + } + catch { }; + return null; } public override VFSItem GetItem(string path) @@ -100,7 +106,7 @@ { string ffrom = Path.Combine(basePath, from); string realbase = Path.GetDirectoryName(ffrom); - string tto= Path.Combine(realbase, to); + string tto = Path.Combine(realbase, to); try { Directory.Move(ffrom, tto); @@ -112,6 +118,21 @@ } } + public override VFSItem Create(string path, bool asFolder) + { + string fspath = Path.Combine(basePath, path); + if (asFolder) + { + Directory.CreateDirectory(fspath); + } + else + { + Directory.CreateDirectory(Path.GetDirectoryName(fspath)); + File.WriteAllBytes(fspath, new byte[0]); + } + return GetItem(path); + } + private void FSDeleteFolder(string path) { foreach (string d in Directory.GetDirectories(path)) @@ -125,5 +146,6 @@ { return Path.Combine(basePath, path); } + } } diff --git a/Sunfish/Sunfish/Middleware/VFSItem.cs b/Sunfish/Sunfish/Middleware/VFSItem.cs index 6911dab..1d1f9eb 100644 --- a/Sunfish/Sunfish/Middleware/VFSItem.cs +++ b/Sunfish/Sunfish/Middleware/VFSItem.cs @@ -51,6 +51,11 @@ return Folder.Rename(Path, newName); } + public VFSItem Create(string name, bool asFolder) + { + return Folder.Create(System.IO.Path.Combine(Path, name), asFolder); + } + public string Path { get; } public string Name { get; } public bool Directory { get; } diff --git a/Sunfish/Sunfish/Program.cs b/Sunfish/Sunfish/Program.cs index fa2d796..35b2bcf 100644 --- a/Sunfish/Sunfish/Program.cs +++ b/Sunfish/Sunfish/Program.cs @@ -7,7 +7,7 @@ { static class Program { - public static string VERSION = "2.0b3"; + public static string VERSION = "2.0b5"; private static Form1 mainform; /// /// Punto de entrada principal para la aplicación. diff --git a/Sunfish/Sunfish/Services/WebService.cs b/Sunfish/Sunfish/Services/WebService.cs index b55cf8b..2daab56 100644 --- a/Sunfish/Sunfish/Services/WebService.cs +++ b/Sunfish/Sunfish/Services/WebService.cs @@ -63,6 +63,8 @@ action = null; else if (string.IsNullOrEmpty(action)) action = null; + if (call.Request.HttpMethod == "PUT") + action = "upload"; switch (action) { case null: @@ -77,6 +79,12 @@ case "rename": ProcessRename(path, call); break; + case "open": + ProcessOpen(path, call); + break; + case "upload": + ProcessUpload(path, call); + break; default: call.HTTPBadRequest(); break; @@ -193,6 +201,60 @@ call.Write("KO"); } + private void ProcessOpen(string path, HttpCall call) + { + VFSItem fil = vfs.GetItem(path); + if (fil != null) + { + string fpath = ((VFSFolderFileSystem)fil.Folder).GetFSPath(fil.Path); + System.Diagnostics.Process.Start(fpath); + call.Write("OK"); + } + else + call.Write("KO"); + } + + private void ProcessUpload(string path, HttpCall call) + { + string soffset = call.Request.Headers["X-Sunfish-Offset"]; + string slength = call.Request.Headers["X-Sunfish-Length"]; + if (string.IsNullOrEmpty(soffset)) + soffset = call.Parameters["offset"]; + if (string.IsNullOrEmpty(slength)) + slength = call.Parameters["length"]; + int pos, len; + if (string.IsNullOrEmpty(soffset) || string.IsNullOrEmpty(slength) || !int.TryParse(soffset, out pos) || !int.TryParse(slength, out len)) + { + call.Write("KO: No offset or length"); + return; + } + try + { + VFSItem fil = vfs.GetItem(path); + if (fil == null) + fil = vfs.Create(path); + if (fil.Directory) + { + call.Write("KO: Exists as directory"); + return; + } + using (Stream s = fil.OpenWrite()) + { + s.Position = pos; + using (Stream sin = call.Request.InputStream) + { + s.TransferFrom(sin, len); + } + } + call.Write("OK"); + } + catch (Exception e) + { + call.Write("KO: " + e.GetType().Name + ":" + e.Message); + } + } + + public void WriteIcon(Icon image, HttpCall call) { call.Response.ContentType = "image/vnd.microsoft.icon"; @@ -284,7 +346,27 @@ WebUI.InitResources(); Dictionary data = new Dictionary(); data["Breadcrumb"] = GetBreadcrumb(path); - //data["Actions"] = actions; + data["Actions"] = new WebUILink[] { + readOnly?null:new WebUILink() + { + Icon="create_new_folder", + Tooltip="New folder", + Click="sunfish.newFolder(this)", + }, + readOnly?null:new WebUILink() + { + Icon="note_add", + Tooltip="New file", + Click="sunfish.newFile(this)", + }, + readOnly?null:new WebUILink() + { + Icon="upload", + Tooltip="Upload. Drop files or fonders here", + Click="sunfish.uploadFile(this)", + //Style="upload-drop", + } + }; data["Items"] = items; data["Include"] = ""; WebUI.WriteTemplate("directory-index", call, data);