## FileAPI A set of javascript tools for working with files. ### Get started ```html
Choose files
``` --- ### Setup options Edit the file `crossdomain.xml` and place it to the root of the domain to which files will be uploaded. ```html ``` --- ### getFiles(input`:HTMLInputElement|Event|$.Event`)`:Array` Retrieve file list from `input` element or `event` object, also support `jQuery`. * input — `HTMLInputElement`, `change` and `drop` event, `jQuery` collection or `jQuery.Event` ```js var el = document.getElement('my-input'); FileAPI.event.on(el, function (evt/**Event*/){ // Retrieve file list var files = FileAPI.getFiles(el); // or event var files = FileAPI.getFiles(evt); }); ``` --- ### getInfo(file`:Object`, callback`:Function`)`:void` Get info of file (see also: FileAPI.addInfoReader). * file — file object (https://developer.mozilla.org/en-US/docs/DOM/File) * callback — function, called after collected info of file ```js // Get info of image file (FileAPI.exif.js included) FileAPI.getInfo(file, function (err/**String*/, info/**Object*/){ if( !err ){ console.log(info); // { width: 800, height: 600, exif: {..} } } }); // Get info of mp3 file (FileAPI.id3.js included) FileAPI.getInfo(file, function (err/**String*/, info/**Object*/){ if( !err ){ console.log(info); // { title: "...", album: "...", artists: "...", ... } } }); ``` --- ### filterFiles(files`:Array`, filter`:Function`, callback`:Function`)`:void` Filtering the list of files, with additional information about files. See also: FileAPI.getInfo and FileAPI.addInfoReader. * files — original list of files * filter — function, takes two arguments: `file` — the file itself, `info` — additional information. * callback — function: `list` — files that match the condition, `other` — all the rest. ```js // Get list of file var files = FileAPI.getFiles(input); // Filter the List FileAPI.filterFiles(files, function (file/**Object*/, info/**Object*/){ if( /^image/.test(file.type) && info ){ return info.width > 320 && info.height > 240; } else { return file.size < 20 * FileAPI.MB; } }, function (list/**Array*/, other/**Array*/){ if( list.length ){ // .. } }); ``` --- ### getDropFiles(evt`:Event|$.Event`, callback`:Function`)`:void` Get a list of files, including directories. * evt — `drop` event * callback — function, takes one argument, a list of files ```js FileAPI.event.on(document, 'drop', function (evt/**Event*/){ evt.preventDefault(); // Get a list of files FileAPI.getDropFiles(evt, function (files/**Array*/){ // ... }); }); ``` --- ### upload(opts`:Object`)`:XmlHttpRequestPromise` Uploading files to the server (`serial: true` by default). Returns XHR-like object. It is important to remember to correctly worked flash-transport server response body must not be empty, for example, you can pass, just text "ok". * opts — options object, see [Upload options](#options) ```js var el = document.getElementById('my-input'); FileAPI.event.on(el, 'change', function (evt/**Event*/){ var files = FileAPI.getFiles(evt); var xhr = FileAPI.upload({ url: 'http://rubaxa.org/FileAPI/server/ctrl.php', files: { file: files[0] }, complete: function (err, xhr){ if( !err ){ var result = FileAPI.parseJSON(xhr.responseText); // ... } } }); }); ``` --- ### upload(url`:String`, files`:Mixed`[, opts`:Object`])`:XmlHttpRequestPromise` * url — a string containing the URL to which the request is sent * files — Array, Object or HTMLInput ```js FileAPI.upload('./ctpl.php', files) .progress(function (evt){ var p = (evt.loaded / evt.total) }) .done(function (xhr){ var result = FileAPI.parseJSON(xhr.responseText); // ... }) ; ``` --- ### load(url`:String`[, opts`:Object`])`:XmlHttpRequestPromise` Load a remote file, by default as `Blob`. * url — a string containing the URL to which the request is sent * opts — request options: { headers, data, withCredentials, responseType: "blob" } ```js var xhr = FileAPI.load("/files/dino.png") .progress(function (evt/**Event*/, xhr/**XmlHttpRequest*/){ var p = (evt.loaded / evt.total); }) .done(function (blob/**Blob*/, xhr/**XmlHttpRequest*/){ // Success }) .fail(function (statusText, xhr/**XmlHttpRequest*/){ alert('Error: '+statusText); }) ; ``` --- ### saveAs(file`:String|File|Blob`[, name`:String`])`:XmlHttpRequestPromise` Save file on disk. * file — a string containing the URL or File/Blob object * name — a file name ```js FileAPI.saveAs("/files/dino.png") .progress(function (evt){ var p = (evt.loaded / evt.total); }) .done(function (){ // Success }) .fail(function (err){ alert('Error: '+err); }) ; ``` --- ### addInfoReader(mime`:RegExp`, handler`:Function`)`:void` Adds a handler for the collection of information about a file. See also: FileAPI.getInfo and FileAPI.filterFiles. * mime — pattern of mime-type * handler — takes two arguments: `file` object and `complete` function callback ```js FileAPI.addInfoReader(/^image/, function (file/**File*/, callback/**Function*/){ // http://www.nihilogic.dk/labs/exif/exif.js // http://www.nihilogic.dk/labs/binaryajax/binaryajax.js FileAPI.readAsBinaryString(file, function (evt/**Object*/){ if( evt.type == 'load' ){ var binaryString = evt.result; var oFile = new BinaryFile(binaryString, 0, file.size); var exif = EXIF.readFromBinaryFile(oFile); callback(false, { 'exif': exif || {} }); } else if( evt.type == 'error' ){ callback('read_as_binary_string'); } else if( evt.type == 'progress' ){ // ... } }); }); ``` --- ### readAsDataURL(file`:Object`, callback`:Function`)`:void` Reading the contents of the specified `File` as `dataURL`. * file — file object * callback — function, receives a result ```js FileAPI.readAsDataURL(file, function (evt/**Object*/){ if( evt.type == 'load' ){ // Success var dataURL = evt.result; } else if( evt.type =='progress' ){ var pr = evt.loaded/evt.total * 100; } else { // Error } }) ``` --- ### readAsBinaryString(file`:Object`, callback`:Function`)`:void` Reading the contents of the specified `File` as `BinaryString`. * file — file object * callback — function, receives a result ```js FileAPI.readAsBinaryString(file, function (evt/**Object*/){ if( evt.type == 'load' ){ // Success var binaryString = evt.result; } else if( evt.type =='progress' ){ var pr = evt.loaded/evt.total * 100; } else { // Error } }) ``` --- ### readAsArrayBuffer(file`:Object`, callback`:Function`)`:void` Reading the contents of the specified `File` as `ArrayBuffer`. * file — file object * callback — function, receives a result ```js FileAPI.readAsArrayBuffer(file, function (evt/**Object*/){ if( evt.type == 'load' ){ // Success var arrayBuffer = evt.result; } else if( evt.type =='progress' ){ var pr = evt.loaded/evt.total * 100; } else { // Error } }) ``` --- ### readAsText(file`:Object`, callback`:Function`)`:void` Reading the contents of the specified `File` as `text`. * file — file object * callback — function, receives a result ```js FileAPI.readAsText(file, function (evt/**Object*/){ if( evt.type == 'load' ){ // Success var text = evt.result; } else if( evt.type =='progress' ){ var pr = evt.loaded/evt.total * 100; } else { // Error } }) ``` --- ### readAsText(file`:Object`, encoding`:String`, callback`:Function`)`:void` Reading the contents of the specified `File` as `text`. * encoding — a string indicating the encoding to use for the returned data. By default, UTF-8. ```js FileAPI.readAsText(file, "utf-8", function (evt/**Object*/){ /*__*/ }) ``` --- ## Upload options ### url`:String` A string containing the URL to which the request is sent. --- ### data`:Object` Additional post data to be sent along with the file uploads. ```js var xhr = FileAPI.upload({ url: '...', data: { 'session-id': 123 }, files: { ... }, }); ``` --- ### headers`:Object` Additional request headers, `HTML5 or Flash` only. ```js var xhr = FileAPI.upload({ url: '...', headers: { 'x-upload': 'fileapi' }, files: { .. }, }); ``` --- ### files`:Object|Array` Key-value object, `key` — post name, `value` — File or FileAPI.Image object. Or an array of files. ```js var xhr = FileAPI.upload({ url: '...', files: { audio: files } }); ``` --- ### postName`:String` The parameter name for the file form data (the request argument name). Default: 'files'. ```js var xhr = FileAPI.upload({ url: '...', files: myFiles, postName: 'images' }); ``` --- ### serial`:Boolean` Serially uploading files to the server. Default: 'true'. `false` — only HTML5, all files are uploading to the server a single request. ```js var xhr = FileAPI.upload( './ctrl.php' , files , { serial: false } // false — only HTML5 ); ``` --- ### parallel`:Number` Number of parallel file uploads. Default: 0. Only HTML5. ```js var xhr = FileAPI.upload( './ctrl.php' , files , { parallel: 3 } // только HTML5. ); ``` --- ### chunkSize`:Number` Chunk size in bytes, HTML5 only. ```js var xhr = FileAPI.upload({ url: '...', files: { images: fileList }, chunkSize: 0.5 * FileAPI.MB }); ``` --- ### chunkUploadRetry`:Number` Number of retries during upload chunks, HTML5 only. ```js var xhr = FileAPI.upload({ url: '...', files: { images: fileList }, chunkSize: 0.5 * FileAPI.MB, chunkUploadRetry: 3 }); ``` -- ### imageTransform`:Object` Rules of changes the original image on the client. ```js var xhr = FileAPI.upload({ url: '...', files: { image: imageFiles }, // Changes the original image imageTransform: { // resize by max side maxWidth: 800, maxHeight: 600, // Add watermark overlay: [{ x: 10, y: 10, src: '/i/watemark.png', rel: FileAPI.Image.RIGHT_BOTTOM }] } }); ``` -- ### imageTransform`:Object` Rules of image transformation on the client, for more images. ```js var xhr = FileAPI.upload({ url: '...', files: { image: imageFiles }, imageTransform: { // resize by max side 'huge': { maxWidth: 800, maxHeight: 600 }, // crop & resize 'medium': { width: 320, height: 240, preview: true }, // crop & resize + watemark 'small': { width: 100, height: 100, preview: true, // Add watermark overlay: [{ x: 5, y: 5, src: '/i/watemark.png', rel: FileAPI.Image.RIGHT_BOTTOM }] } } }); ``` -- ### imageTransform`:Object` Convert all images to jpeg or png. ```js var xhr = FileAPI.upload({ url: '...', files: { image: imageFiles }, imageTransform: { type: 'image/jpeg', quality: 0.86 // jpeg quality } }); ``` ### imageOriginal`:Boolean` Sent to the server the original image or not, if defined imageTransform option. -- ### imageAutoOrientation`:Boolean` Auto-rotate images on the basis of EXIF. -- ### prepare`:Function` Prepare options upload for a particular file. ```js var xhr = FileAPI.upload({ url: '...', files: { .. } prepare: function (file/**Object*/, options/**Object*/){ options.data.secret = utils.getSecretKey(file.name); } }); ``` -- ### upload`:Function` Start uploading. ```js var xhr = FileAPI.upload({ url: '...', files: { .. } upload: function (xhr/**Object*/, options/**Object*/){ // ... } }); ``` -- ### fileupload`:Function` Start file uploading. ```js var xhr = FileAPI.upload({ url: '...', files: { .. } uploadfile: function (file/**Object*/, xhr/**Object*/, options/**Object*/){ // ... } }); ``` -- ### progress`:Function` Callback for upload progress events. ```js var xhr = FileAPI.upload({ url: '...', files: { .. } progress: function (evt/**Object*/, file/**Object*/, xhr/**Object*/, options/**Object*/){ var pr = evt.loaded/evt.total * 100; } }); ``` -- ### fileprogress`:Function` Callback for upload file progress events. ```js var xhr = FileAPI.upload({ url: '...', files: { .. } fileprogress: function (evt/**Object*/, file/**Object*/, xhr/**Object*/, options/**Object*/){ var pr = evt.loaded/evt.total * 100; } }); ``` -- ### complete`:Function` Callback for end upload requests. ```js var xhr = FileAPI.upload({ url: '...', files: { .. } complete: function (err/**String*/, xhr/**Object*/, file/**Object/, options/**Object*/){ if( !err ){ // All files successfully uploaded. } } }); ``` -- ### filecomplete`:Function` Callback for end upload requests. ```js var xhr = FileAPI.upload({ url: '...', files: { .. } filecomplete: function (err/**String*/, xhr/**Object*/, file/**Object/, options/**Object*/){ if( !err ){ // File successfully uploaded var result = xhr.responseText; } } }); ``` --- ## File object ### name`:String` The name of the file referenced by the File object. ### type`:String` The type (MIME type) of the file referenced by the File object. ### size`:Number` The size (in bytes) of the file referenced by the File object. --- ## XmlHttpRequestPromise ### status`:Number` The status of the response to the request. This is the HTTP result code (for example, status is 200 for a successful request). --- ### statusText`:String` The response string returned by the HTTP server. Unlike status, this includes the entire text of the response message ("200 OK", for example). --- ### responseXML`:Document` The response to the request as a DOM Document object, or null if the request was unsuccessful, has not yet been sent, or cannot be parsed as XML or HTML. The response is parsed as if it were a text/xml stream. When the responseType is set to "document" and the request has been made asynchronously, the response is parsed as it were a text/html stream. --- ### responseText`:String` The response to the request as text, or null if the request was unsuccessful or has not yet been sent. --- ### abort()`:void` Aborts the request if it has already been sent. --- ### getAllResponseHeaders()`:String` Returns all the response headers as a string, or null if no response has been received. --- ### getResponseHeader(name`:String`)`:String` Returns the string containing the text of the specified header, or null if either the response has not yet been received or the header doesn't exist in the response. * name — header name --- ### success(callback`:Function`)`:XmlHttpRequestPromise` A function to be called if the request succeeds. * callback — receives two arguments: `xhr` and `options` --- ### error(callback`:Function`)`:XmlHttpRequestPromise` A function to be called if the request failure. * callback — receives three arguments: `error`, `xhr` and `options` --- ## FileAPI.event ### on(el`:HTMLElement`, events`:String`, handler`:Function`)`:void` Attach an event handler function. * el — DOM element * events — one or more space-separated event types. * handler — A function to execute when the event is triggered. --- ### off(el`:HTMLElement`, events`:String`, handler`:Function`)`:void` Remove an event handler. * el — DOM element * events — one or more space-separated event types. * handler — a handler function previously attached for the event(s). --- ### one(el`:HTMLElement`, events`:String`, handler`:Function`)`:void` Attach an event handler function. The handler is executed at most once. * el — DOM element * events — one or more space-separated event types. * handler — a function to execute when the event is triggered. --- ### dnd(el`:HTMLElement`, hover`:Function`, handler`:Function`)`:void` Attach an drag and drop event handler function. * el — drop zone * hover — `dragenter` and `dragleave` listener * handler — `drop` event handler. ```js var el = document.getElementById('dropzone'); FileAPI.event.dnd(el, function (over){ el.style.backgroundColor = over ? '#f60': ''; }, function (files){ if( files.length ){ // Upload their. } }); // or jQuery $('#dropzone').dnd(hoverFn, dropFn); ``` --- ### dnd.off(el`:HTMLElement`, hover`:Function`, handler`:Function`)`:void` Remove an drag and drop event handler function. * el — drop zone * hover — `dragenter` and `dragleave` listener * handler — `drop` event handler. ```js // Native FileAPI.event.dnd.off(el, hoverFn, dropFn); // jQuery $('#dropzone').dndoff(hoverFn, dropFn); ``` -- ## FileAPI.Image Class for working with images ### constructor(file`:Object`)`:void` The constructor takes a single argument, the `File` object. * file — the `File` object ```js FileAPI.Image(imageFile).get(function (err/**String*/, img/**HTMLElement*/){ if( !err ){ document.body.appendChild( img ); } }); ``` --- ### crop(width`:Number`, height`:Number`)`:FileAPI.Image` Crop image by width and height. * width — new image width * height — new image height ```js FileAPI.Image(imageFile) .crop(640, 480) .get(function (err/**String*/, img/**HTMLElement*/){ }) ; ``` ### crop(x`:Number`, y`:Number`, width`:Number`, height`:Number`)`:FileAPI.Image` Crop image by x, y, width and height. * x — offset from the left corner * y — offset from the top corner ```js FileAPI.Image(imageFile) .crop(100, 50, 320, 240) .get(function (err/**String*/, img/**HTMLElement*/){ }) ; ``` --- ### resize(width`:Number`, height`:Number`[, strategy`:String`])`:FileAPI.Image` Resize image. * width — new image width * height — new image height * strategy — enum: `min`, `max`, `preview`. By default `undefined`. ```js FileAPI.Image(imageFile) .resize(320, 240) .get(function (err/**String*/, img/**HTMLElement*/){ }) ; // Resize image on by max side. FileAPI.Image(imageFile) .resize(320, 240, 'max') .get(function (err/**String*/, img/**HTMLElement*/){ }) ; ``` --- ### preview(width`:Number`[, height`:Number`])`:FileAPI.Image` Crop and resize image. * width — new image width * height — new image height ```js FileAPI.Image(imageFile) .preview(100, 100) .get(function (err/**String*/, img/**HTMLElement*/){ }) ; ``` --- ### rotate(deg`:Number`)`:FileAPI.Image` Rotate image. * deg — rotation angle in degrees ```js FileAPI.Image(imageFile) .rotate(90) .get(function (err/**String*/, img/**HTMLElement*/){ }) ; ``` --- ### rotate(type`:String`)`:FileAPI.Image` Auto rotate image by exif. * type — "auto" ```js FileAPI.Image(imageFile) .rotate("auto") .get(function (err/**String*/, img/**HTMLElement*/){ }) ; ``` --- ### filter(callback`:Function`)`:FileAPI.Image` Apply filter function. Only `HTML5`. * callback — takes two arguments, `canvas` element and `done` method. ```js FileAPI.Image(imageFile) .filter(function (canvas/**HTMLCanvasElement*/, doneFn/**Function*/){ // bla-bla-lba doneFn(); }) .get(function (err/**String*/, img/**HTMLElement*/){ }) ; ``` --- ### filter(name`:String`)`:FileAPI.Image` Uses [CamanJS](http://camanjs.com/), include it before FileAPI library. * name — CamanJS filter name (custom or preset) ```js Caman.Filter.register("my-funky-filter", function () { // http://camanjs.com/guides/#Extending }); FileAPI.Image(imageFile) .filter("my-funky-filter") // or .filter("vintage") .get(function (err/**String*/, img/**HTMLElement*/){ }) ; ``` --- ### overlay(images`:Array`)`:FileAPI.Image` Add overlay images, eg: watermark. * images — array of overlays ```js FileAPI.Image(imageFile) .overlay([ // Left corner. { x: 10, y: 10, w: 100, h: 10, src: '/i/watermark.png' }, // Right bottom corner. { x: 10, y: 10, src: '/i/watermark.png', rel: FileAPI.Image.RIGHT_BOTTOM } ]) .get(function (err/**String*/, img/**HTMLElement*/){ }) ; ``` --- ### get(fn`:Function`)`:FileAPI.Image` Get the final image. * fn — complete callback --- ## FileAPI.Camera To work with a webcam, be sure to set `FileAPI.media: true`. ### publish(el`:HTMLElement`, options`:Object`, callback`:Function`)`:void` Publication of the camera. * el — target * options — { `width: 100%`, `height: 100%`, `start: true` } * callback — the first parameter is a possible error, the second instance of FileAPI.Camera ```js var el = document.getElementById('cam'); FileAPI.Camera.publish(el, { width: 320, height: 240 }, function (err, cam/**FileAPI.Camera*/){ if( !err ){ // The webcam is ready, you can use it. } }); ``` --- ### start(callback`:Function`)`:void` Turn on the camera. * callback — will be called when the camera ready ```js var el = document.getElementById('cam'); FileAPI.Camera.publish(el, { start: false }, function (err, cam/**FileAPI.Camera*/){ if( !err ){ // Turn on cam.start(function (err){ if( !err ){ // The camera is ready for use. } }); } }); ``` --- ### stop()`:void` Turn off the camera. --- ### shot()`:FileAPI.Image` Take a picture with the camera. ```js var el = document.getElementById('cam'); FileAPI.Camera.publish(el, function (err, cam/**FileAPI.Camera*/){ if( !err ){ var shot = cam.shot(); // take a picture // create thumbnail 100x100 shot.preview(100).get(function (err, img){ previews.appendChild(img); }); // and/or FileAPI.upload({ url: '...', files: { cam: shot }); } }); ``` --- ## Сonstants ### FileAPI.KB`:Number` 1024 bytes ### FileAPI.MB`:Number` 1048576 bytes ### FileAPI.GB`:Number` 1073741824 bytes ### FileAPI.TB`:Number` 1.0995116e+12 bytes --- ## Utils ### FileAPI.each(obj`:Object|Array`, callback`:Function`[, thisObject`:Mixed`])`:void` Iterate over a object or array, executing a function for each matched element. * obj — array or object * callback — a function to execute for each element. * thisObject — object to use as `this` when executing `callback`. -- ### FileAPI.extend(dst`:Object`, src`:Object`)`:Object` Merge the contents of two objects together into the first object. * dst — an object that will receive the new properties * src — an object containing additional properties to merge in. -- ### FileAPI.filter(array`:Array`, callback`:Function`[, thisObject`:Mixed`)`:Object` Creates a new array with all elements that pass the test implemented by the provided function. * array — original Array * callback — Function to test each element of the array. * thisObject — object to use as `this` when executing `callback`. --- ## Support ### FileAPI.support.html5`:Boolean` HTML5 borwser support ### FileAPI.support.cors`:Boolean` This cross-origin resource sharing is used to enable cross-site HTTP requests. ### FileAPI.support.dnd`:Boolean` Drag'n'drop events support. ### FileAPI.support.flash`:Boolean` Availability Flash plugin. ### FileAPI.support.canvas`:Boolean` Canvas support. ### FileAPI.support.dataURI`:Boolean` Support dataURI as src for image. ### FileAPI.support.chunked`:Boolean` Support chuncked upload. ### FileAPI.support.saveAs`:Boolean` Support `navigator.msSaveBlob`. ### FileAPI.support.download`:Boolean` Support for attribute `download` in `a[href]`. ### FileAPI.support.accept`:Boolean` Support for attribute `accept` in `input[type="file"]`. ### FileAPI.support.multiple`:Boolean` Support for attribute `multiple` in `input[type="file"]`. --- ## Flash Flash is very "buggy" thing :] The server response can not be empty. Therefore, in the event of a successful uploading `http status` should be only `200 OK`. ### Settings Flash settings. It is advisable to place flash on the same server where the files will be uploaded. ```html ``` --- ### crossdomain.xml Necessarily make this file on the server. Do not forget to replace `youdomain.com` on the name of your domain. ```xml ``` --- ### request The following sample HTTP POST request is sent from Flash Player to a server-side script if no parameters are specified: ```xml POST /server/ctrl.php HTTP/1.1 Accept: text/* Content-Type: multipart/form-data; boundary=----------Ij5ae0ae0KM7GI3KM7 User-Agent: Shockwave Flash Host: www.youdomain.com Content-Length: 421 Connection: Keep-Alive Cache-Control: no-cache ------------Ij5GI3GI3ei4GI3ei4KM7GI3KM7KM7 Content-Disposition: form-data; name="Filename" MyFile.jpg ------------Ij5GI3GI3ei4GI3ei4KM7GI3KM7KM7 Content-Disposition: form-data; name="Filedata"; filename="MyFile.jpg" Content-Type: application/octet-stream [[..FILE_DATA_HERE..]] ------------Ij5GI3GI3ei4GI3ei4KM7GI3KM7KM7 Content-Disposition: form-data; name="Upload" Submit Query ------------Ij5GI3GI3ei4GI3ei4KM7GI3KM7KM7-- ``` --- ## Server settings ### IFrame/JSONP ```php FileAPI::OK , 'statusText' => 'OK' , 'body' => array('count' => sizeof($files) ), $jsonp); exit; } ?> ``` --- ### CORS Enable CORS. ```php ``` --- ### Chunked file upload Client and server communicate to each other using the following HTTP headers and status codes.
Client explicitly sets the following headers:
Any other headers are set by a target browser and are not used by client. Library does not provide any facilities to track a file uniqueness across requests, it's left on developer's consideration.
Response codes: For recoverable errors server tries to resend chunk `chunkUploadRetry` times then fails.
  • X-Last-Known-Byte: int, library tries to resend chunk from the given offset. Applicable to response codes 200 and 416
  • All the other codes - fatal error, user's involvement is recommend. --- ## Buttons examples ### Base Simple input[type="file"] ```html ``` --- ### Button Stylized button. ```html
    Upload files
    ``` --- ### Link Button like link. ```html Upload photo ``` --- ## Installation, testing, assembling `npm install fileapi`
    `cd fileapi`
    `npm install`
    `grunt` --- ## Changelog ### 2.1.0 ### 2.0.4 ### 2.0.3 ### 2.0.2 ### 2.0.1 ### 2.0.0 ### 1.2.6 ### 1.2.5 ### 1.2.4 ### 1.2.3 ### 1.2.2 ### 1.2.1 ### 1.2.0 ### 1.1.0 ### 1.0.1 ### 1.0.0