diff --git a/lib/storage.pl b/lib/storage.pl index 8e48e4f2..44e86273 100644 --- a/lib/storage.pl +++ b/lib/storage.pl @@ -52,7 +52,8 @@ storage_load_term/2, % +Hash, -Term use_gitty_file/1, % +File - use_gitty_file/2 % +File, +Options + use_gitty_file/2, % +File, +Options + open_gittystore/1 ]). :- use_module(library(http/http_dispatch)). :- use_module(library(http/http_parameters)). @@ -71,7 +72,7 @@ :- use_module(library(dcg/basics)). :- use_module(library(pcre)). :- use_module(library(pengines_io)). - +:- use_module(upload). :- use_module(page). :- use_module(gitty). :- use_module(patch). diff --git a/lib/upload.pl b/lib/upload.pl new file mode 100644 index 00000000..e1145dde --- /dev/null +++ b/lib/upload.pl @@ -0,0 +1,58 @@ +:- module(upload, [handle_upload/1]). +:- use_module(library(http/thread_httpd)). +:- use_module(library(http/http_dispatch)). +:- use_module(library(http/http_header)). +:- use_module(library(http/http_multipart_plugin)). +:- use_module(library(http/http_client)). +:- use_module(library(http/html_write)). +:- use_module(library(option)). +:- use_module(library(debug)). +:- use_module(web_storage). +:- use_module(gitty). + +:- http_handler(root(upload), handle_upload, [method(post)]). + +handle_upload(Request) :- + multipart_post_request(Request), !, + http_read_data(Request, Parts, [on_filename(save_file)]), + memberchk(file=file(FileName, TempFile), Parts), + save_file_to_storage(FileName, TempFile, SavedPath), + format('Content-type: application/json~n~n'), + format('{"status":"success","filename":"~w","saved":"~w"}', [FileName, SavedPath]). +handle_upload(_Request) :- + throw(http_reply(bad_request(bad_file_upload))). + +multipart_post_request(Request) :- + memberchk(method(post), Request), + memberchk(content_type(ContentType), Request), + http_parse_header_value( + content_type, ContentType, + media(multipart/'form-data', _)). + +:- public save_file/3. + +save_file(In, file(FileName, TempFile), Options) :- + option(filename(FileName), Options), + setup_call_cleanup( + tmp_file_stream(octet, TempFile, Out), + copy_stream_data(In, Out), + close(Out)). + +save_file_to_storage(FileName, TempFile, SavedPath) :- + web_storage:open_gittystore(Dir), + setup_call_cleanup( + open(TempFile, read, In, [type(binary)]), + read_string(In, _, Data), + close(In) + ), + Meta = _{author: 'user', public: true}, + gitty:gitty_create(Dir, FileName, Data, Meta, _Commit), + directory_file_path(Dir, FileName, SavedPath). + +% error message +:- multifile prolog:message//1. + +prolog:message(bad_file_upload) --> + [ 'A file upload must be submitted as multipart/form-data using', nl, + 'name=file and providing a file-name' + ]. diff --git a/web/js/jswish.js b/web/js/jswish.js index e14e575e..50645735 100644 --- a/web/js/jswish.js +++ b/web/js/jswish.js @@ -123,6 +123,9 @@ preferences.setInform("preserve-state", ".unloadable"); "Download": glyph("floppy-save", function() { menuBroadcast("download"); }), + "Upload ...": glyph("upload", function() { + $('#fileInput').trigger('click'); + }), "Print ...": glyph("print", function() { menuBroadcast("print"); }) @@ -180,6 +183,72 @@ preferences.setInform("preserve-state", ".unloadable"); } }; // defaults; + $(() => { + if ($('#fileInput').length === 0) { + $('body').append(''); + } + + $(document).on('change', '#fileInput', function(event) { + var files = event.target.files; + if (files.length > 0) { + var allowedExtensions = ['.swib', '.pl']; + + var uploadFile = function(file, callback) { + var formData = new FormData(); + formData.append('file', file); + + $.ajax({ + url: '/upload', + type: 'POST', + data: formData, + processData: false, + contentType: false, + success: function(response) { + console.log('File uploaded successfully:', response); + callback(null, response.filename); + }, + error: function(error) { + console.error('File upload failed:', error); + callback(error); + } + }); + }; + + var lastUploadedFileName = null; + + var uploadNextFile = function(index) { + if (index < files.length) { + var file = files[index]; + var fileName = file.name; + var fileExtension = fileName.slice(fileName.lastIndexOf('.')).toLowerCase(); + + if (allowedExtensions.includes(fileExtension)) { + uploadFile(file, function(err, fileName) { + if (!err) { + lastUploadedFileName = fileName; + uploadNextFile(index + 1); + } else { + alert('File upload failed for file: ' + fileName); + } + }); + } else { + alert('Invalid file type: ' + fileName + '. Only .swib and .pl files are allowed.'); + uploadNextFile(index + 1); + } + } else { + // redirect to last file + if (lastUploadedFileName) { + window.location.href = '/p/' + lastUploadedFileName; + } else { + location.reload(); + } + } + }; + + uploadNextFile(0); + } + }); +}); /** @lends $.fn.swish */ var methods = {