### Run Node.js Sample with Express Source: https://github.com/flowjs/flow.js/blob/master/samples/Node.js/README.md Instructions to install and run the Node.js sample application for flow.js using npm and Node.js. This sample requires the Express framework for cleaner code and will upload files to the 'samples/Node.js/tmp' directory. ```bash cd samples/Node.js npm install node app.js ``` -------------------------------- ### Install Flow.js via npm Source: https://github.com/flowjs/flow.js/blob/master/README.md This command installs the latest version of the Flow.js library using the npm package manager. Ensure you have Node.js and npm installed on your system. ```bash npm install @flowjs/flow.js ``` -------------------------------- ### PHP: Create Temporary Directory for File Uploads Source: https://github.com/flowjs/flow.js/blob/master/samples/Backend on PHP.md This PHP code snippet creates a temporary directory to store uploaded file chunks. It utilizes `mkdir` with specific permissions (0777) and the `true` flag to recursively create parent directories if they do not exist. This ensures a dedicated space for fragmented uploads. ```php mkdir($temp_dir, 0777, true); ``` -------------------------------- ### Complete HTML/JavaScript File Upload UI with Flow.js Source: https://context7.com/flowjs/flow.js/llms.txt This snippet provides a full HTML and JavaScript example for creating a file upload interface. It utilizes Flow.js for handling the upload logic, including drag-and-drop support, progress indication, and upload controls. The UI elements are styled with basic CSS. ```html Flow.js Upload Demo

Flow.js File Upload

Drop files here
``` -------------------------------- ### Flow.js Chunk Testing (GET Requests) Source: https://github.com/flowjs/flow.js/blob/master/README.md Details how to implement `GET` requests to test if chunks have already been uploaded, enabling upload resilience. ```APIDOC ## Handling GET Requests for Chunk Testing ### Description When the `testChunks` option is enabled in Flow.js, a corresponding `GET` request can be implemented on the server to verify if chunks have already been successfully uploaded. This allows uploads to resume after browser restarts or interruptions. ### Method GET ### Endpoint `/upload` (or your configured upload endpoint, with same parameters as POST) ### Parameters #### Query Parameters - **flowChunkNumber** (integer) - Required - The index of the chunk to test. - **flowTotalChunks** (integer) - Required - The total number of chunks for the file. - **flowChunkSize** (integer) - Required - The size of each chunk in bytes. - **flowTotalSize** (integer) - Required - The total size of the file in bytes. - **flowIdentifier** (string) - Required - A unique identifier for the file. - **flowFilename** (string) - Required - The original filename. - **flowRelativePath** (string) - Optional - The relative path of the file. ### Response #### Success Response (2xx) - **200, 201, 202**: Accepted - Indicates that the specified chunk (or all preceding chunks) has been completed. The client will skip uploading this chunk. #### Error Response - **Permanent Error Status Codes**: Upload is stopped. - **Other Status Codes**: The chunk will be uploaded via a standard POST request. ### Note Implementing this `GET` request endpoint significantly improves the user experience by allowing uploads to resume efficiently without re-uploading already processed data. ``` -------------------------------- ### Haskell: Initiate Resumable File Upload Source: https://github.com/flowjs/flow.js/blob/master/samples/Backend on Haskell.md This function handles the initial request to start a resumable file upload. It validates the filename and file size, creates an upload token, and prepares the file on the server. Dependencies include `ActionRoute`, `POST`, `pathJSON`, `withAuth`, `getVolume`, `runForm`, and various `deform` functions for input validation. ```Haskell {-# LANGUAGE OverloadedStrings #} module Databrary.Controller.Upload ( uploadStart , uploadChunk , testChunk ) where import Control.Exception (bracket) import Control.Monad ((<=<)) import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Class (lift) import qualified Data.ByteString as BS import qualified Data.ByteString.Unsafe as BSU import Data.ByteString.Lazy.Internal (defaultChunkSize) import Data.Int (Int64) import Data.Maybe (isJust) import Data.Word (Word64) import Foreign.C.Types (CSize(..)) import Foreign.Marshal.Array (allocaArray, peekArray) import Foreign.Ptr (castPtr) import Network.HTTP.Types (ok200, noContent204, badRequest400) import qualified Network.Wai as Wai import System.IO (SeekMode(AbsoluteSeek)) import System.Posix.Files.ByteString (setFdSize) import System.Posix.IO.ByteString (openFd, OpenMode(ReadOnly, WriteOnly), defaultFileFlags, exclusive, closeFd, fdSeek, fdWriteBuf, fdReadBuf) import System.Posix.Types (COff(..)) import Databrary.Has (view, peek, peeks, focusIO) import qualified Databrary.JSON as JSON import Databrary.Service.Log import Databrary.Model.Id import Databrary.Model.Permission import Databrary.Model.Volume import Databrary.Model.Format import Databrary.Model.Token import Databrary.Store.Upload import Databrary.Store.Asset import Databrary.HTTP.Form.Deform import Databrary.HTTP.Path.Parser import Databrary.Action.Response import Databrary.Action import Databrary.Controller.Paths import Databrary.Controller.Form import Databrary.Controller.Volume import Control.Monad.IO.Class fileSizeForm :: DeformActionM f Int64 fileSizeForm = deformCheck "Invalid file size." (0 <) =<< deform uploadStart :: ActionRoute (Id Volume) uploadStart = action POST (pathJSON >/> pathId withAuth $ do liftIO $ print "inside of uploadStart..." --DEBUG vol <- getVolume PermissionEDIT vi liftIO $ print "vol assigned...running form..." --DEBUG (filename, size) <- runForm Nothing $ (,) <$> ("filename" .:> (deformCheck "File format not supported." (isJust . getFormatByFilename) =<< deform)) <*> ("size" .:> (deformCheck "File too large." ((maxAssetSize >=) . fromIntegral) =<< fileSizeForm)) liftIO $ print "creating Upload..." --DEBUG tok <- createUpload vol filename size liftIO $ print "peeking..." --DEBUG file <- peeks $ uploadFile tok liftIO $ bracket (openFd file WriteOnly (Just 0o640) defaultFileFlags{ exclusive = True }) closeFd (`setFdSize` COff size) return $ okResponse [] $ unId (view tok :: Id Token) ``` -------------------------------- ### PHP: Move Uploaded File Chunk Source: https://github.com/flowjs/flow.js/blob/master/samples/Backend on PHP.md This PHP code moves an uploaded file chunk from its temporary location to its final destination. It uses the `move_uploaded_file` function, which is the recommended method for handling file uploads in PHP. If the move operation fails, it logs an error message including the chunk number and filename. ```php if (!move_uploaded_file($file['tmp_name'], $dest_file)) { _log('Error saving (move_uploaded_file) chunk '.$_POST['flowChunkNumber'].' for file '.$_POST['flowFilename']); } else { // check if all the parts present, and create the final destination file createFileFromChunks($temp_dir, $_POST['flowFilename'], $_POST['flowChunkSize'], $_POST['flowTotalSize']); } ``` -------------------------------- ### PHP Flow.js Chunked File Upload Server Implementation Source: https://github.com/flowjs/flow.js/blob/master/samples/Backend on PHP.md This PHP script implements the server-side logic for Flow.js, enabling file uploads in chunks. It handles receiving, storing, and reassembling file parts into a final file. It includes functions for logging, recursively deleting directories, and creating the final file from its constituent parts. The script is not recommended for production use without modifications for security (e.g., cleaning file names). ```php `. Once all * the parts have been uploaded, a final destination file is * being created from all the stored parts (appending one by one). * * @author Gregory Chris (http://online-php.com) * @email www.online.php@gmail.com */ //////////////////////////////////////////////////////////////////// // THE FUNCTIONS //////////////////////////////////////////////////////////////////// /** * * Logging operation - to a file (upload_log.txt) and to the stdout * @param string $str - the logging string */ function _log($str) { // log to the output $log_str = date('d.m.Y').": {$str}\r\n"; echo $log_str; // log to file if (($fp = fopen('upload_log.txt', 'a+')) !== false) { fputs($fp, $log_str); fclose($fp); } } /** * * Delete a directory RECURSIVELY * @param string $dir - directory path * @link http://php.net/manual/en/function.rmdir.php */ function rrmdir($dir) { if (is_dir($dir)) { $objects = scandir($dir); foreach ($objects as $object) { if ($object != "." && $object != "..") { if (filetype($dir . "/" . $object) == "dir") { rrmdir($dir . "/" . $object); } else { unlink($dir . "/" . $object); } } } reset($objects); rmdir($dir); } } /** * * Check if all the parts exist, and * gather all the parts of the file together * @param string $dir - the temporary directory holding all the parts of the file * @param string $fileName - the original file name * @param string $chunkSize - each chunk size (in bytes) * @param string $totalSize - original file size (in bytes) */ function createFileFromChunks($temp_dir, $fileName, $chunkSize, $totalSize) { // count all the parts of this file $total_files = 0; foreach(scandir($temp_dir) as $file) { if (stripos($file, $fileName) !== false) { $total_files++; } } // check that all the parts are present // the size of the last part is between chunkSize and 2*$chunkSize if ($total_files * $chunkSize >= ($totalSize - $chunkSize + 1)) { // create the final destination file if (($fp = fopen('temp/'.$fileName, 'w')) !== false) { for ($i=1; $i<=$total_files; $i++) { fwrite($fp, file_get_contents($temp_dir.'/'.$fileName.'.part'.$i)); _log('writing chunk '.$i); } fclose($fp); } else { _log('cannot create the destination file'); return false; } // rename the temporary directory (to avoid access from other // concurrent chunks uploads) and than delete it if (rename($temp_dir, $temp_dir.'_UNUSED')) { rrmdir($temp_dir.'_UNUSED'); } else { rrmdir($temp_dir); } } } //////////////////////////////////////////////////////////////////// // THE SCRIPT //////////////////////////////////////////////////////////////////// //check if request is GET and the requested chunk exists or not. this makes testChunks work if ($_SERVER['REQUEST_METHOD'] === 'GET') { $temp_dir = 'temp/'.$_GET['flowIdentifier']; $chunk_file = $temp_dir.'/'.$_GET['flowFilename'].'.part'.$_GET['flowChunkNumber']; if (file_exists($chunk_file)) { header("HTTP/1.0 200 Ok"); } else { header("HTTP/1.0 404 Not Found"); } } // loop through files and move the chunks to a temporarily created directory if (!empty($_FILES)) foreach ($_FILES as $file) { // check the error status if ($file['error'] != 0) { _log('error '.$file['error'].' in file '.$_POST['flowFilename']); continue; } // init the destination file (format .part<#chunk> // the file is stored in a temporary directory $temp_dir = 'temp/'.$_POST['flowIdentifier']; $dest_file = $temp_dir.'/'.$_POST['flowFilename'].'.part'.$_POST['flowChunkNumber']; // create the temporary directory if (!is_dir($temp_dir)) { ``` -------------------------------- ### Initialize Flow.js Instance with Configuration Source: https://context7.com/flowjs/flow.js/llms.txt Creates a new Flow.js instance, configuring upload targets, chunk size, simultaneous uploads, and query parameters. It also checks for browser compatibility. Ensure the target URL is correctly set and server-side upload handling is implemented. ```javascript var flow = new Flow({ target: '/api/upload', chunkSize: 1024 * 1024, // 1MB chunks simultaneousUploads: 3, testChunks: true, query: { upload_token: 'my_secure_token', user_id: '12345' }, headers: { 'X-Custom-Header': 'value' } }); // Check if browser supports Flow.js if (!flow.support) { alert('Your browser does not support HTML5 file uploads'); location.href = '/fallback-uploader'; } ``` -------------------------------- ### Flow.js Initialization and Event Handling (JavaScript) Source: https://github.com/flowjs/flow.js/blob/master/samples/Node.js/public/index.html Initializes the Flow.js library for file uploads, configures upload parameters, and sets up event listeners for various upload stages like file addition, progress, success, and errors. It also handles browser support checks and assigns file upload handlers to UI elements. ```javascript (function () { var r = new Flow({ target: '/upload', chunkSize: 1024*1024, testChunks: false }); // Flow.js isn't supported, fall back on a different method if (!r.support) { $('.flow-error').show(); return ; } // Show a place for dropping/selecting files $('.flow-drop').show(); r.assignDrop($('.flow-drop')[0]); r.assignBrowse($('.flow-browse')[0]); r.assignBrowse($('.flow-browse-folder')[0], true); r.assignBrowse($('.flow-browse-image')[0], false, false, {accept: 'image/*'}); // Handle file add event r.on('fileAdded', function(file){ // Show progress bar $('.flow-progress, .flow-list').show(); // Add the file to the list $('.flow-list').append( '
  • ' + 'Uploading ' + ' ' + ' ' + '' + 'Download' + ' ' + '' + ' ' + '' + '' + ' ' + '' + '' + ' ' + '' ); var $self = $('.flow-file-'+file.uniqueIdentifier); $self.find('.flow-file-name').text(file.name); $self.find('.flow-file-size').text(readablizeBytes(file.size)); $self.find('.flow-file-download').attr('href', '/download/' + file.uniqueIdentifier).hide(); $self.find('.flow-file-pause').on('click', function () { file.pause(); $self.find('.flow-file-pause').hide(); $self.find('.flow-file-resume').show(); }); $self.find('.flow-file-resume').on('click', function () { file.resume(); $self.find('.flow-file-pause').show(); $self.find('.flow-file-resume').hide(); }); $self.find('.flow-file-cancel').on('click', function () { file.cancel(); $self.remove(); }); }); r.on('filesSubmitted', function(file) { r.upload(); }); r.on('complete', function(){ // Hide pause/resume when the upload has completed $('.flow-progress .progress-resume-link, .flow-progress .progress-pause-link').hide(); }); r.on('fileSuccess', function(file,message){ var $self = $('.flow-file-'+file.uniqueIdentifier); // Reflect that the file upload has completed $self.find('.flow-file-progress').text('(completed)'); $self.find('.flow-file-pause, .flow-file-resume').remove(); $self.find('.flow-file-download').attr('href', '/download/' + file.uniqueIdentifier).show(); }); r.on('fileError', function(file, message){ // Reflect that the file upload has resulted in error $('.flow-file-'+file.uniqueIdentifier+' .flow-file-progress').html('(file could not be uploaded: '+message+')'); }); r.on('fileProgress', function(file){ // Handle progress for both the file and the overall upload $('.flow-file-'+file.uniqueIdentifier+' .flow-file-progress') .html(Math.floor(file.progress()*100) + '% ' + readablizeBytes(file.averageSpeed) + '/s ' + secondsToStr(file.timeRemaining()) + ' remaining') ; $('.progress-bar').css({width:Math.floor(r.progress()*100) + '%'}); }); r.on('uploadStart', function(){ // Show pause, hide resume $('.flow-progress .progress-resume-link').hide(); $('.flow-progress .progress-pause-link').show(); }); r.on('catchAll', function() { console.log.apply(console, arguments); }); window.r = { pause: function () { r.pause(); // Show resume, hide pause $('.flow-file-resume').show(); $('.flow-file-pause').hide(); $('.flow-progress .progress-resume-link').show(); $('.flow-progress .progress-pause-link').hide(); }, cancel: function() { r.cancel(); $('.flow-file').remove(); }, upload: function() { $('.flow-file-pause').show(); $('.flow-file-resume').hide(); r.resume(); }, flow: r }; })(); ``` -------------------------------- ### Resumable.js Client-Side Options for Octet Uploads Source: https://github.com/flowjs/flow.js/blob/master/samples/java/README.md Configuration options for initializing Resumable.js on the client-side to work with a server that supports 'octet' uploads. Key settings include the target upload URL, chunk size, simultaneous uploads, and enabling `testChunks` for resumability. ```JavaScript var r = new Resumable({ target:'/test/upload', chunkSize:1*1024*1024, simultaneousUploads:4, testChunks: true, throttleProgressCallbacks:1, method: "octet" }); ``` -------------------------------- ### Enable Cross-domain Uploads in Node.js Source: https://github.com/flowjs/flow.js/blob/master/samples/Node.js/README.md Configuration steps to enable cross-domain uploads for the Node.js flow.js sample. This involves uncommenting specific lines in app.js to allow 'Access-Control-Allow-Origin' and updating the target URL in public/index.html. ```javascript // In app.js (uncomment lines 24-31 and line 17) // Example: Enabling CORS app.all('*', function(req, res, next) { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "X-Requested-With"); next(); }); // In public/index.html (update target on line 49) // Example: target: 'http://www.example.com/upload' ``` -------------------------------- ### Tcl Procedure for Handling Resumable Uploads in AOLserver Source: https://github.com/flowjs/flow.js/blob/master/samples/Backend on AOLserver and OpenACS.md This Tcl procedure, `handle_resumable_file`, is designed to process resumable file uploads initiated by Flow.js. It validates incoming chunks, checks file sizes against limits, saves individual chunks, and concatenates them into a final file upon completion. It also handles GET requests to check the status of existing chunks. ```tcl ad_proc handle_resumable_file { {-file_parameter_name "file"} {-folder "/tmp"} -check_video:boolean {-max_file_size ""} } {} { # Check parameter to see if this is indeed a resumable ad_page_contract {} { {resumableChunkNumber:integer "0"} {resumableChunkSize:integer "0"} {resumableTotalSize:integer "0"} {resumableIdentifier ""} {resumableFilename ""} } # Clean up the identifier regsub -all {[^0-9A-Za-z_-]} $resumableIdentifier "" resumableIdentifier # Check if the request is sane if { $resumableChunkNumber==0 || $resumableChunkSize==0 || $resumableTotalSize==0 || $resumableIdentifier eq "" } { return "non_resumable_request" } set number_of_chunks [expr int(floor($resumableTotalSize/($resumableChunkSize*1.0)))] if { $number_of_chunks==0 } {set number_of_chunks 1} if { $resumableChunkNumber>$number_of_chunks } { return "invalid_resumable_request" } # What would the file name be? set filename [file join $folder "resumable-${resumableIdentifier}.${resumableChunkNumber}"] # If this is a GET request, we should tell the uploader if the file is already in place or not if { [ns_conn method] eq "GET" } { if { [file exists $filename] && [file size $filename]==$resumableChunkSize } { doc_return 200 text/plain "ok" } else { doc_return 204 text/plain "not found" } ad_script_abort } # Assign a tmp file ad_page_contract {} [list "${file_parameter_name}:trim" "${file_parameter_name}.tmpfile:tmpfile"] set tmp_filename [set "${file_parameter_name}.tmpfile"] if { $resumableFilename ne "" } { set original_filename $resumableFilename } else { set original_filename [set $file_parameter_name] } # Check data size if { $max_file_size ne "" && $resumableTotalSize>$max_file_size } { return [list "invalid_resumable_request" "The file is too large" $original_filename] } elseif { $resumableChunkNumber<$number_of_chunks && [file size $tmp_filename]!=$resumableChunkSize } { return [list "invalid_resumable_request" "Wrong data size" $original_filename] } elseif { $number_of_chunks>1 && $resumableChunkNumber==$number_of_chunks && [file size $tmp_filename] != [expr ($resumableTotalSize % $resumableChunkSize) + $resumableChunkSize] } { return [list "invalid_resumable_request" "Wrong data size" $original_filename] } elseif { $number_of_chunks==1 && [file size $tmp_filename] != $resumableTotalSize } { return [list "invalid_resumable_request" "Wrong data size" $original_filename] } # Save the chunk file mkdir $folder file copy -force $tmp_filename $filename # Try collating the first and last chunk -- and identify if { $check_video_p && ($resumableChunkNumber==1 || $resumableChunkNumber==$number_of_chunks) } { ## (Here you can do check on first and last chunk if needed ## For example, we will check if this is a support video file.) } # Check if all chunks have come in set chunk_num 1 set chunk_files [list] while { $chunk_num<=$number_of_chunks } { set chunk_filename [file join $folder "resumable-${resumableIdentifier}.${chunk_num}"] if { ![file exists $chunk_filename] } { return [list "partly_done" $filename $original_filename] } lappend chunk_files $chunk_filename incr chunk_num } # We've come this far, meaning that all the pieces are in place set output_filename [file join $folder "resumable-${resumableIdentifier}.final"] foreach file $chunk_files { exec cat $file >> $output_filename catch { file delete $file } } return [list "done" $output_filename $original_filename $resumableIdentifier] } ``` -------------------------------- ### Utility Functions for File Uploads (JavaScript) Source: https://github.com/flowjs/flow.js/blob/master/samples/Node.js/public/index.html Provides utility functions to format byte sizes into human-readable strings (e.g., KB, MB, GB) and to convert a duration in seconds into a human-readable string format (e.g., minutes, hours, days). These are used for displaying file progress and estimated time remaining. ```javascript function readablizeBytes(bytes) { var s = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB']; var e = Math.floor(Math.log(bytes) / Math.log(1024)); return (bytes / Math.pow(1024, e)).toFixed(2) + " " + s[e]; } function secondsToStr (temp) { function numberEnding (number) { return (number > 1) ? 's' : ''; } var years = Math.floor(temp / 31536000); if (years) { return years + ' year' + numberEnding(years); } var days = Math.floor((temp %= 31536000) / 86400); if (days) { return days + ' day' + numberEnding(days); } var hours = Math.floor((temp %= 86400) / 3600); if (hours) { return hours + ' hour' + numberEnding(hours); } var minutes = Math.floor((temp %= 3600) / 60); if (minutes) { return minutes + ' minute' + numberEnding(minutes); } var seconds = temp % 60; return seconds + ' second' + numberEnding(seconds); } ``` -------------------------------- ### PHP Server for Chunked File Uploads with Flow.js Source: https://context7.com/flowjs/flow.js/llms.txt This PHP script handles chunked file uploads from Flow.js. It supports checking if a chunk already exists via GET requests and receiving/storing uploaded chunks via POST requests. After all chunks are received, it merges them into a single file and cleans up temporary directories. Dependencies include basic PHP file system functions. ```php = ($total_size - $chunk_size + 1)) { // All chunks received, merge them $final_file = 'uploads/' . $_POST['flowFilename']; $fp = fopen($final_file, 'w'); for ($i = 1; $i <= $total_files; $i++) { $chunk_path = $temp_dir . '/' . $_POST['flowFilename'] . '.part' . $i; fwrite($fp, file_get_contents($chunk_path)); unlink($chunk_path); } fclose($fp); rmdir($temp_dir); } header("HTTP/1.0 200 Ok"); } } ?> ``` -------------------------------- ### Configuration Options Source: https://github.com/flowjs/flow.js/blob/master/README.md Configuration options for Flow.js instances, affecting upload behavior and response handling. ```APIDOC ## Configuration Options ### `chunkSize` The size of each chunk in bytes. For larger files, smaller chunk sizes can improve reliability. (Default: 1MB) ### `autoUpload` Automatically upload files as they are added. (Default: false) ### `maxChunkRetries` Maximum number of retries for a chunk upload before failing. (Default: 3) ### `chunkRetryInterval` Time in seconds to wait before retrying a chunk upload. (Default: undefined) ### `simultaneousUploads` Maximum number of simultaneous chunk uploads. Setting this to 1.0 means that the average upload speed will be equal to the current upload speed. For longer file uploads it is better set this number to 0.02, because time remaining estimation will be more accurate. This parameter must be adjusted together with `progressCallbacksInterval` parameter. (Default: 3) ### `progressCallbacksInterval` Interval in milliseconds for progress callbacks. (Default: 0.1) ### `successStatuses` Response is success if response status is in this list. (Default: `[200,201,202]`) ### `permanentErrors` Response fails if response status is in this list. (Default: `[404, 415, 500, 501]`) ``` -------------------------------- ### Initialize Flow.js Upload Object Source: https://github.com/flowjs/flow.js/blob/master/README.md This JavaScript code snippet demonstrates how to create a new Flow object. It specifies the target URL for uploads and can include additional query parameters. It also includes a check for Flow.js support, falling back to an alternative method if not supported. ```javascript var flow = new Flow({ target:'/api/photo/redeem-upload-token', query:{upload_token:'my_token'} }); // Flow.js isn't supported, fall back on a different method if(!flow.support) location.href = '/some-old-crappy-uploader'; ``` -------------------------------- ### Control Upload Flow with Flow.js Source: https://context7.com/flowjs/flow.js/llms.txt Demonstrates how to control the overall upload process using methods like upload(), pause(), resume(), and cancel(). It also shows how to check the upload status and retrieve progress information. This snippet assumes a 'flow' object is already initialized. ```javascript document.getElementById('uploadBtn').addEventListener('click', function() { flow.upload(); }); document.getElementById('pauseBtn').addEventListener('click', function() { flow.pause(); }); document.getElementById('resumeBtn').addEventListener('click', function() { flow.resume(); }); document.getElementById('cancelBtn').addEventListener('click', function() { flow.cancel(); }); if (flow.isUploading()) { console.log('Upload in progress'); } var progress = flow.progress(); console.log('Upload progress:', Math.floor(progress * 100) + '%'); console.log('Total size:', flow.getSize(), 'bytes'); console.log('Uploaded:', flow.sizeUploaded(), 'bytes'); console.log('Time remaining:', flow.timeRemaining(), 'seconds'); ``` -------------------------------- ### Flow.js Server-Side Chunk Handling Source: https://github.com/flowjs/flow.js/blob/master/README.md Explains the parameters sent with each chunk upload and the expected server responses for managing upload state. ```APIDOC ## Server-Side Chunk Handling for Flow.js ### Description Flow.js requires server-side logic to reassemble file chunks. This section details the parameters sent with each chunk and the appropriate HTTP status codes for server responses. ### Method POST ### Endpoint `/upload` (or your configured upload endpoint) ### Parameters #### Query Parameters - **flowChunkNumber** (integer) - Required - The index of the current chunk (starts at 1). - **flowTotalChunks** (integer) - Required - The total number of chunks for the file. - **flowChunkSize** (integer) - Required - The size of each chunk in bytes. - **flowTotalSize** (integer) - Required - The total size of the file in bytes. - **flowIdentifier** (string) - Required - A unique identifier for the file. - **flowFilename** (string) - Required - The original filename. - **flowRelativePath** (string) - Optional - The relative path of the file when uploading a directory. ### Response #### Success Response (2xx) - **200, 201, 202**: Accepted - The chunk was received correctly and no re-upload is needed. #### Error Response - **404, 415, 500, 501**: Permanent Error - The file is not supported; cancel the entire upload. - **Other codes**: Retry - An error occurred, but the chunk should be re-uploaded. ### Note Allow for chunks to be uploaded multiple times, as Flow.js is designed to handle unstable network conditions. The last chunk's data size may be smaller than `flowChunkSize`. ``` -------------------------------- ### Go Flow.js Upload Handler Source: https://github.com/flowjs/flow.js/blob/master/samples/Backend on Go.md This Go code implements the backend for handling Flow.js chunked uploads. It defines routes for checking chunk existence and receiving uploaded chunks. The handler parses multipart form data, saves chunks to disk, and orchestrates the assembly of the complete file upon receiving the final chunk. It assumes the last chunk is the final piece of the file. ```go package main import ( "bytes" "github.com/codegangsta/martini" "github.com/codegangsta/martini-contrib/render" "io" "io/ioutil" "net/http" "os" "sort" "strconv" "strings" ) var completedFiles = make(chan string, 100) func main() { for i := 0; i < 3; i++ { go assembleFile(completedFiles) } m := martini.Classic() m.Use(render.Renderer(render.Options{ Layout: "layout", Delims: render.Delims{"{\[[", "]]}"}, Extensions: []string{”.html”}})) m.Get("/", func(r render.Render) { r.HTML(200, "index", nil) }) m.Post("/upload", streamHandler(chunkedReader)) m.Get("/upload", continueUpload) m.Run() } type ByChunk []os.FileInfo func (a ByChunk) Len() int { return len(a) } func (a ByChunk) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByChunk) Less(i, j int) bool { ai, _ := strconv.Atoi(a[i].Name()) aj, _ := strconv.Atoi(a[j].Name()) return ai < aj } type streamHandler func(http.ResponseWriter, *http.Request) error func (fn streamHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := fn(w, r); err != nil { http.Error(w, err.Error(), 500) } } func continueUpload(w http.ResponseWriter, r *http.Request) { chunkDirPath := "./incomplete/" + r.FormValue("flowFilename") + "/" + r.FormValue("flowChunkNumber") if _, err := os.Stat(chunkDirPath); err != nil { w.WriteHeader(204) return } } func chunkedReader(w http.ResponseWriter, r *http.Request) error { r.ParseMultipartForm(25) chunkDirPath := "./incomplete/" + r.FormValue("flowFilename") err := os.MkdirAll(chunkDirPath, 02750) if err != nil { return err } for _, fileHeader := range r.MultipartForm.File["file"] { src, err := fileHeader.Open() if err != nil { return err } defer src.Close() dst, err := os.Create(chunkDirPath + "/" + r.FormValue("flowChunkNumber")) if err != nil { return err } defer dst.Close() io.Copy(dst, src) fileInfos, err := ioutil.ReadDir(chunkDirPath) if err != nil { return err } cT, err := strconv.Atoi(chunkTotal) if err != nil { return err } if len(fileInfos) == cT { completedFiles <- chunkDirPath } } return nil } func assembleFile(jobs <-chan string) { for path := range jobs { fileInfos, err := ioutil.ReadDir(path) if err != nil { return } // create final file to write to dst, err := os.Create(strings.Split(path, "/")[2]) if err != nil { return } defer dst.Close() sort.Sort(ByChunk(fileInfos)) for _, fs := range fileInfos { src, err := os.Open(path + "/" + fs.Name()) if err != nil { return } defer src.Close() io.Copy(dst, src) } os.RemoveAll(path) } } ``` -------------------------------- ### Flow.js Properties Source: https://github.com/flowjs/flow.js/blob/master/README.md Properties available on a Flow.js instance. ```APIDOC ## Properties ### `.support` A boolean value indicating whether or not Flow.js is supported by the current browser. ### `.supportDirectory` A boolean value, which indicates if the browser supports directory uploads. ### `.opts` A hash object of the configuration of the Flow.js instance. ### `.files` An array of `FlowFile` file objects added by the user. ``` -------------------------------- ### Flow.js Methods Source: https://github.com/flowjs/flow.js/blob/master/README.md Methods available on a Flow.js instance for managing uploads. ```APIDOC ## Methods ### `.assignBrowse(domNodes, isDirectory, singleFile, attributes)` Assign a browse action to one or more DOM nodes. - `domNodes`: array of DOM nodes or a single node. - `isDirectory`: Pass `true` to allow directories to be selected (Chrome only, support can be checked with `supportDirectory` property). - `singleFile`: Set to `true` to prevent multiple file uploads. Also look at config parameter `singleFile`. - `attributes`: Pass an object of keys and values to set custom attributes on input fields. For example, you can set `accept` attribute to `image/*`. This means that the user will be able to select only images. Full list of attributes: https://www.w3.org/wiki/HTML/Elements/input/file. Note: avoid using `a` and `button` tags as file upload buttons, use span instead. ### `.assignDrop(domNodes)` Assign one or more DOM nodes as a drop target. ### `.unAssignDrop(domNodes)` Unassign one or more DOM nodes as a drop target. ### `.on(event, callback)` Listen for an event from Flow.js. ### `.off([event, [callback]])` - `.off()`: All events are removed. - `.off(event)`: Remove all callbacks of a specific event. - `.off(event, callback)`: Remove a specific callback of an event. `callback` should be a `Function`. ### `.upload()` Start or resume uploading. ### `.pause()` Pause uploading. ### `.resume()` Resume uploading. ### `.cancel()` Cancel upload of all `FlowFile` objects and remove them from the list. ### `.progress()` Returns a float between 0 and 1 indicating the current upload progress of all files. ### `.isUploading()` Returns a boolean indicating whether or not the instance is currently uploading anything. ### `.addFile(file)` Add an HTML5 `File` object to the list of files. ### `.removeFile(file)` Cancel upload of a specific `FlowFile` object from the list. ### `.getFromUniqueIdentifier(uniqueIdentifier)` Look up a `FlowFile` object by its unique identifier. ### `.getSize()` Returns the total size of the upload in bytes. ### `.sizeUploaded()` Returns the total size uploaded of all files in bytes. ### `.timeRemaining()` Returns the remaining time to upload all files in seconds. Accuracy is based on average speed. If speed is zero, time remaining will be equal to `Number.POSITIVE_INFINITY`. ```