├── LICENSE ├── README.md ├── demo.html ├── package.json ├── tarball.js └── tests ├── files ├── simple.tar └── simple │ ├── hello.txt │ └── tux.png ├── index.html └── tests.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ankit Rohatgi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tarballjs 2 | Javascript library to create or read tar files in the browser 3 | 4 | ## Why? 5 | It is often necessary to pack data into single files in the browser (e.g. creating a "project" file consisting of images+data in WebPlotDigitizer). One way is to create simple tarballs with all the data. There are a few existing libraries that do this, but this seems easy enough to do so I decided to do make my own library for learning purposes. 6 | 7 | ## Status 8 | Refer to https://www.npmjs.com/package/@gera2ld/tarjs for a better maintained version of this code. This code is mainly used to support WebPlotDigitizer. 9 | 10 | There are a few known limitations with this library: 11 | 12 | - Browser only, no support for NodeJS. 13 | - File name (including path) has to be less than 100 characters. 14 | - Maximum total file size seems to be limited to somewhere between 500MB to 1GB (exact limit is unknown). 15 | 16 | Some benefits of using this library: 17 | 18 | - Code is a lot cleaner than most other implementations that I can find. 19 | - Unit tests for read and write. 20 | 21 | ## Browser Support 22 | This works fine on any recent version of Chrome, Firefox or Safari. 23 | 24 | ## Running Unit Tests 25 | For Chrome, the test page has to be hosted on a HTTP server. An easy way is to use Python: 26 | 27 | In the root directory of this project, do: 28 | 29 | python3 -m http.server 8000 30 | 31 | Then browse to http://localhost:8000/tests/ 32 | 33 | In Firefox, you can simply load tests/index.html without starting a web server. 34 | 35 | ## Other Implementations 36 | 37 | - https://github.com/beatgammit/tar-js 38 | - https://github.com/chriswininger/jstar 39 | - https://github.com/InvokIT/js-untar (reading only) 40 | - https://github.com/workhorsy/uncompress.js (reading only) 41 | - http://stuk.github.io/jszip/ (active and well maintained project but for .zip files) 42 | 43 | If you are aware of other implementations, then please let me know :) 44 | 45 | ## References 46 | 47 | - https://en.wikipedia.org/wiki/Tar_(computing) 48 | - https://www.gnu.org/software/tar/manual/html_node/Standard.html 49 | 50 | 51 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 45 | 46 | 47 |

tarballjs

48 |
49 |

Read .tar

50 |

File to extract:

51 |
52 |

Files:

53 | 55 |

56 |
57 |

Generate .tar

58 |

Load File(s):

59 |
60 | https://github.com/ankitrohatgi/tarballjs 61 | 62 | 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tarballjs", 3 | "version": "1.0.0", 4 | "description": "Read and write tarballs in the browser", 5 | "main": "tarball.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ankitrohatgi/tarballjs.git" 15 | }, 16 | "keywords": [ 17 | "tar" 18 | ], 19 | "author": "Ankit Rohatgi", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/ankitrohatgi/tarballjs/issues" 23 | }, 24 | "homepage": "https://github.com/ankitrohatgi/tarballjs#readme" 25 | } 26 | -------------------------------------------------------------------------------- /tarball.js: -------------------------------------------------------------------------------- 1 | let tarball = {}; 2 | 3 | if (typeof module === "object" && typeof module.exports === "object") { 4 | // CommonJS 5 | module.exports = tarball; 6 | } else if (typeof this === "object") { 7 | // Browser 8 | // use this instead of window, since window might not exist and throw and error 9 | this.tarball = tarball; 10 | } 11 | 12 | tarball.TarReader = class { 13 | constructor() { 14 | this.fileInfo = []; 15 | } 16 | 17 | readFile(file) { 18 | return new Promise((resolve, reject) => { 19 | let reader = new FileReader(); 20 | reader.onload = (event) => { 21 | this.buffer = event.target.result; 22 | this.fileInfo = []; 23 | this._readFileInfo(); 24 | resolve(this.fileInfo); 25 | }; 26 | reader.readAsArrayBuffer(file); 27 | }); 28 | } 29 | 30 | readArrayBuffer(arrayBuffer) { 31 | this.buffer = arrayBuffer; 32 | this.fileInfo = []; 33 | this._readFileInfo(); 34 | return this.fileInfo; 35 | } 36 | 37 | _readFileInfo() { 38 | this.fileInfo = []; 39 | let offset = 0; 40 | let file_size = 0; 41 | let file_name = ""; 42 | let file_type = null; 43 | while(offset < this.buffer.byteLength - 512) { 44 | file_name = this._readFileName(offset); // file name 45 | if(file_name.length == 0) { 46 | break; 47 | } 48 | file_type = this._readFileType(offset); 49 | file_size = this._readFileSize(offset); 50 | 51 | this.fileInfo.push({ 52 | "name": file_name, 53 | "type": file_type, 54 | "size": file_size, 55 | "header_offset": offset 56 | }); 57 | 58 | offset += (512 + 512*Math.trunc(file_size/512)); 59 | if(file_size % 512) { 60 | offset += 512; 61 | } 62 | } 63 | } 64 | 65 | getFileInfo() { 66 | return this.fileInfo; 67 | } 68 | 69 | _readString(str_offset, size) { 70 | let strView = new Uint8Array(this.buffer, str_offset, size); 71 | let i = strView.indexOf(0); 72 | let td = new TextDecoder(); 73 | return td.decode(strView.slice(0, i)); 74 | } 75 | 76 | _readFileName(header_offset) { 77 | let name = this._readString(header_offset, 100); 78 | return name; 79 | } 80 | 81 | _readFileType(header_offset) { 82 | // offset: 156 83 | let typeView = new Uint8Array(this.buffer, header_offset+156, 1); 84 | let typeStr = String.fromCharCode(typeView[0]); 85 | if(typeStr == "0") { 86 | return "file"; 87 | } else if(typeStr == "5") { 88 | return "directory"; 89 | } else { 90 | return typeStr; 91 | } 92 | } 93 | 94 | _readFileSize(header_offset) { 95 | // offset: 124 96 | let szView = new Uint8Array(this.buffer, header_offset+124, 12); 97 | let szStr = ""; 98 | for(let i = 0; i < 11; i++) { 99 | szStr += String.fromCharCode(szView[i]); 100 | } 101 | return parseInt(szStr,8); 102 | } 103 | 104 | _readFileBlob(file_offset, size, mimetype) { 105 | let view = new Uint8Array(this.buffer, file_offset, size); 106 | let blob = new Blob([view], {"type": mimetype}); 107 | return blob; 108 | } 109 | 110 | _readFileBinary(file_offset, size) { 111 | let view = new Uint8Array(this.buffer, file_offset, size); 112 | return view; 113 | } 114 | 115 | _readTextFile(file_offset, size) { 116 | let view = new Uint8Array(this.buffer, file_offset, size); 117 | let td = new TextDecoder(); 118 | return td.decode(view); 119 | } 120 | 121 | getTextFile(file_name) { 122 | let info = this.fileInfo.find(info => info.name == file_name); 123 | if (info) { 124 | return this._readTextFile(info.header_offset+512, info.size); 125 | } 126 | } 127 | 128 | getFileBlob(file_name, mimetype) { 129 | let info = this.fileInfo.find(info => info.name == file_name); 130 | if (info) { 131 | return this._readFileBlob(info.header_offset+512, info.size, mimetype); 132 | } 133 | } 134 | 135 | getFileBinary(file_name) { 136 | let info = this.fileInfo.find(info => info.name == file_name); 137 | if (info) { 138 | return this._readFileBinary(info.header_offset+512, info.size); 139 | } 140 | } 141 | }; 142 | 143 | tarball.TarWriter = class { 144 | constructor() { 145 | this.fileData = []; 146 | } 147 | 148 | addTextFile(name, text, opts) { 149 | let te = new TextEncoder(); 150 | let arr = te.encode(text); 151 | this.fileData.push({ 152 | name: name, 153 | array: arr, 154 | type: "file", 155 | size: arr.length, 156 | dataType: "array", 157 | opts: opts 158 | }); 159 | } 160 | 161 | addFileArrayBuffer(name, arrayBuffer, opts) { 162 | let arr = new Uint8Array(arrayBuffer); 163 | this.fileData.push({ 164 | name: name, 165 | array: arr, 166 | type: "file", 167 | size: arr.length, 168 | dataType: "array", 169 | opts: opts 170 | }); 171 | } 172 | 173 | addFile(name, file, opts) { 174 | this.fileData.push({ 175 | name: name, 176 | file: file, 177 | size: file.size, 178 | type: "file", 179 | dataType: "file", 180 | opts: opts 181 | }); 182 | } 183 | 184 | addFolder(name, opts) { 185 | this.fileData.push({ 186 | name: name, 187 | type: "directory", 188 | size: 0, 189 | dataType: "none", 190 | opts: opts 191 | }); 192 | } 193 | 194 | _createBuffer() { 195 | let tarDataSize = 0; 196 | for(let i = 0; i < this.fileData.length; i++) { 197 | let size = this.fileData[i].size; 198 | tarDataSize += 512 + 512*Math.trunc(size/512); 199 | if(size % 512) { 200 | tarDataSize += 512; 201 | } 202 | } 203 | let bufSize = 10240*Math.trunc(tarDataSize/10240); 204 | if(tarDataSize % 10240) { 205 | bufSize += 10240; 206 | } 207 | this.buffer = new ArrayBuffer(bufSize); 208 | } 209 | 210 | async download(filename) { 211 | let blob = await this.writeBlob(); 212 | let $downloadElem = document.createElement('a'); 213 | $downloadElem.href = URL.createObjectURL(blob); 214 | $downloadElem.download = filename; 215 | $downloadElem.style.display = "none"; 216 | document.body.appendChild($downloadElem); 217 | $downloadElem.click(); 218 | document.body.removeChild($downloadElem); 219 | } 220 | 221 | async writeBlob(onUpdate) { 222 | return new Blob([await this.write(onUpdate)], {"type":"application/x-tar"}); 223 | } 224 | 225 | write(onUpdate) { 226 | return new Promise((resolve,reject) => { 227 | this._createBuffer(); 228 | let offset = 0; 229 | let filesAdded = 0; 230 | let onFileDataAdded = () => { 231 | filesAdded++; 232 | if (onUpdate) { 233 | onUpdate(filesAdded / this.fileData.length * 100); 234 | } 235 | if(filesAdded === this.fileData.length) { 236 | let arr = new Uint8Array(this.buffer); 237 | resolve(arr); 238 | } 239 | }; 240 | for(let fileIdx = 0; fileIdx < this.fileData.length; fileIdx++) { 241 | let fdata = this.fileData[fileIdx]; 242 | // write header 243 | this._writeFileName(fdata.name, offset); 244 | this._writeFileType(fdata.type, offset); 245 | this._writeFileSize(fdata.size, offset); 246 | this._fillHeader(offset, fdata.opts, fdata.type); 247 | this._writeChecksum(offset); 248 | 249 | // write file data 250 | let destArray = new Uint8Array(this.buffer, offset+512, fdata.size); 251 | if(fdata.dataType === "array") { 252 | for(let byteIdx = 0; byteIdx < fdata.size; byteIdx++) { 253 | destArray[byteIdx] = fdata.array[byteIdx]; 254 | } 255 | onFileDataAdded(); 256 | } else if(fdata.dataType === "file") { 257 | let reader = new FileReader(); 258 | 259 | reader.onload = (function(outArray) { 260 | let dArray = outArray; 261 | return function(event) { 262 | let sbuf = event.target.result; 263 | let sarr = new Uint8Array(sbuf); 264 | for(let bIdx = 0; bIdx < sarr.length; bIdx++) { 265 | dArray[bIdx] = sarr[bIdx]; 266 | } 267 | onFileDataAdded(); 268 | }; 269 | })(destArray); 270 | reader.readAsArrayBuffer(fdata.file); 271 | } else if(fdata.type === "directory") { 272 | onFileDataAdded(); 273 | } 274 | 275 | offset += (512 + 512*Math.trunc(fdata.size/512)); 276 | if(fdata.size % 512) { 277 | offset += 512; 278 | } 279 | } 280 | }); 281 | } 282 | 283 | _writeString(str, offset, size) { 284 | let strView = new Uint8Array(this.buffer, offset, size); 285 | let te = new TextEncoder(); 286 | if (te.encodeInto) { 287 | // let the browser write directly into the buffer 288 | let written = te.encodeInto(str, strView).written; 289 | for (let i = written; i < size; i++) { 290 | strView[i] = 0; 291 | } 292 | } else { 293 | // browser can't write directly into the buffer, do it manually 294 | let arr = te.encode(str); 295 | for (let i = 0; i < size; i++) { 296 | strView[i] = i < arr.length ? arr[i] : 0; 297 | } 298 | } 299 | } 300 | 301 | _writeFileName(name, header_offset) { 302 | // offset: 0 303 | this._writeString(name, header_offset, 100); 304 | } 305 | 306 | _writeFileType(typeStr, header_offset) { 307 | // offset: 156 308 | let typeChar = "0"; 309 | if(typeStr === "file") { 310 | typeChar = "0"; 311 | } else if(typeStr === "directory") { 312 | typeChar = "5"; 313 | } 314 | let typeView = new Uint8Array(this.buffer, header_offset + 156, 1); 315 | typeView[0] = typeChar.charCodeAt(0); 316 | } 317 | 318 | _writeFileSize(size, header_offset) { 319 | // offset: 124 320 | let sz = size.toString(8); 321 | sz = this._leftPad(sz, 11); 322 | this._writeString(sz, header_offset+124, 12); 323 | } 324 | 325 | _leftPad(number, targetLength) { 326 | let output = number + ''; 327 | while (output.length < targetLength) { 328 | output = '0' + output; 329 | } 330 | return output; 331 | } 332 | 333 | _writeFileMode(mode, header_offset) { 334 | // offset: 100 335 | this._writeString(this._leftPad(mode,7), header_offset+100, 8); 336 | } 337 | 338 | _writeFileUid(uid, header_offset) { 339 | // offset: 108 340 | this._writeString(this._leftPad(uid,7), header_offset+108, 8); 341 | } 342 | 343 | _writeFileGid(gid, header_offset) { 344 | // offset: 116 345 | this._writeString(this._leftPad(gid,7), header_offset+116, 8); 346 | } 347 | 348 | _writeFileMtime(mtime, header_offset) { 349 | // offset: 136 350 | this._writeString(this._leftPad(mtime,11), header_offset+136, 12); 351 | } 352 | 353 | _writeFileUser(user, header_offset) { 354 | // offset: 265 355 | this._writeString(user, header_offset+265, 32); 356 | } 357 | 358 | _writeFileGroup(group, header_offset) { 359 | // offset: 297 360 | this._writeString(group, header_offset+297, 32); 361 | } 362 | 363 | _writeChecksum(header_offset) { 364 | // offset: 148 365 | this._writeString(" ", header_offset+148, 8); // first fill with spaces 366 | 367 | // add up header bytes 368 | let header = new Uint8Array(this.buffer, header_offset, 512); 369 | let chksum = 0; 370 | for(let i = 0; i < 512; i++) { 371 | chksum += header[i]; 372 | } 373 | this._writeString(chksum.toString(8), header_offset+148, 8); 374 | } 375 | 376 | _getOpt(opts, opname, defaultVal) { 377 | if(opts != null) { 378 | if(opts[opname] != null) { 379 | return opts[opname]; 380 | } 381 | } 382 | return defaultVal; 383 | } 384 | 385 | _fillHeader(header_offset, opts, fileType) { 386 | let uid = this._getOpt(opts, "uid", 1000); 387 | let gid = this._getOpt(opts, "gid", 1000); 388 | let mode = this._getOpt(opts, "mode", fileType === "file" ? "664" : "775"); 389 | let mtime = this._getOpt(opts, "mtime", Date.now()); 390 | let user = this._getOpt(opts, "user", "tarballjs"); 391 | let group = this._getOpt(opts, "group", "tarballjs"); 392 | 393 | this._writeFileMode(mode, header_offset); 394 | this._writeFileUid(uid.toString(8), header_offset); 395 | this._writeFileGid(gid.toString(8), header_offset); 396 | this._writeFileMtime(Math.trunc(mtime/1000).toString(8), header_offset); 397 | 398 | this._writeString("ustar", header_offset+257,6); // magic string 399 | this._writeString("00", header_offset+263,2); // magic version 400 | 401 | this._writeFileUser(user, header_offset); 402 | this._writeFileGroup(group, header_offset); 403 | } 404 | }; 405 | 406 | -------------------------------------------------------------------------------- /tests/files/simple.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitrohatgi/tarballjs/1134d90ca7f1a9fe00e124a0f0cb4d055a1b9cfa/tests/files/simple.tar -------------------------------------------------------------------------------- /tests/files/simple/hello.txt: -------------------------------------------------------------------------------- 1 | hello world! 🙂 2 | -------------------------------------------------------------------------------- /tests/files/simple/tux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitrohatgi/tarballjs/1134d90ca7f1a9fe00e124a0f0cb4d055a1b9cfa/tests/files/simple/tux.png -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tarballjs 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | let testUtils = { 2 | fetchTar: function(filename) { 3 | return new Promise((resolve, reject) => { 4 | fetch(filename).then(resp => resp.blob()).then((blob) => { 5 | let tar = new tarball.TarReader(); 6 | tar.readFile(blob).then((fileInfo) => { 7 | resolve(tar); 8 | }); 9 | }); 10 | }); 11 | }, 12 | generateTar: function(download) { 13 | // generate a tarball and read it back 14 | return new Promise((resolve, reject) => { 15 | let tarWriter = new tarball.TarWriter(); 16 | tarWriter.addFolder("myfolder/"); 17 | tarWriter.addTextFile("myfolder/first.txt", "this is some text 🙂"); 18 | tarWriter.addTextFile("myfolder/second.txt", "some more text with 🙃 emojis"); 19 | fetch("files/simple/tux.png").then(resp => resp.blob()).then((blob) => { 20 | let file = blob; 21 | file.name = "tux.png"; 22 | file.lastModifiedDate = new Date(); 23 | tarWriter.addFile("myfolder/tux.png", file); 24 | if(download) { 25 | tarWriter.download("generated.tar").then(() => { 26 | resolve(null); 27 | }); 28 | } else { 29 | tarWriter.writeBlob().then((tarBlob) => { 30 | let tarFile = tarBlob; 31 | let tarReader = new tarball.TarReader(); 32 | tarReader.readFile(tarFile).then((fileInfo) => { 33 | resolve(tarReader); 34 | }); 35 | }); 36 | } 37 | }); 38 | }); 39 | } 40 | }; 41 | 42 | // Read tests 43 | QUnit.module("Read Tests"); 44 | QUnit.test( "Count files", function( assert ) { 45 | let done = assert.async(); 46 | testUtils.fetchTar("files/simple.tar").then((tar) => { 47 | let fileInfo = tar.getFileInfo(); 48 | assert.equal(fileInfo.length, 3, "Has 3 files"); 49 | assert.equal(fileInfo[0].name, "simple/", "Has simple directory"); 50 | assert.equal(fileInfo[1].name, "simple/hello.txt", "Has text file"); 51 | assert.equal(fileInfo[2].name, "simple/tux.png", "Has image file"); 52 | done(); 53 | }); 54 | }); 55 | 56 | QUnit.test( "Check file headers", function( assert ) { 57 | let done = assert.async(); 58 | testUtils.fetchTar("files/simple.tar").then((tar) => { 59 | let fileInfo = tar.getFileInfo(); 60 | assert.equal(fileInfo[2].name, "simple/tux.png", "File name is ok"); 61 | assert.equal(fileInfo[2].type, "file", "File type is ok"); 62 | assert.equal(fileInfo[2].size, 11913, "File size is ok"); 63 | done(); 64 | }); 65 | }); 66 | 67 | QUnit.test( "Check text file contents", function( assert ) { 68 | let done = assert.async(); 69 | testUtils.fetchTar("files/simple.tar").then((tar) => { 70 | let text = tar.getTextFile("simple/hello.txt"); 71 | assert.equal(text, "hello world! 🙂\n", "Text file contents are ok"); 72 | done(); 73 | }); 74 | }); 75 | 76 | QUnit.test( "Check image file contents", function( assert ) { 77 | let done = assert.async(); 78 | testUtils.fetchTar("files/simple.tar").then((tar) => { 79 | let imageBlob = tar.getFileBlob("simple/tux.png", "image/png"); 80 | let imageURL = URL.createObjectURL(imageBlob); 81 | let image = new Image(); 82 | image.onload = (event) => { 83 | assert.equal(image.width, 265, "Image width is ok"); 84 | assert.equal(image.height, 314, "Image height is ok"); 85 | done(); 86 | }; 87 | image.src = imageURL; 88 | }); 89 | }); 90 | 91 | // write tests 92 | QUnit.module("Write tests"); 93 | QUnit.test( "Count files", function( assert ) { 94 | let done = assert.async(); 95 | testUtils.generateTar().then((tar) => { 96 | let fileInfo = tar.getFileInfo(); 97 | assert.equal(fileInfo.length, 4, "file count is ok"); 98 | done(); 99 | }); 100 | }); 101 | 102 | QUnit.test( "Check file headers", function( assert ) { 103 | let done = assert.async(); 104 | testUtils.generateTar().then((tar) => { 105 | let fileInfo = tar.getFileInfo(); 106 | assert.equal(fileInfo[3].name, "myfolder/tux.png", "file name is ok"); 107 | assert.equal(fileInfo[3].type, "file", "file type is ok"); 108 | assert.equal(fileInfo[3].size, 11913, "file size is ok"); 109 | done(); 110 | }); 111 | }); 112 | 113 | QUnit.test( "Check text file contents", function( assert ) { 114 | let done = assert.async(); 115 | testUtils.generateTar().then((tar) => { 116 | let text = tar.getTextFile("myfolder/second.txt"); 117 | assert.equal(text, "some more text with 🙃 emojis", "text file contents are ok"); 118 | done(); 119 | }); 120 | }); 121 | 122 | QUnit.test( "Check image file contents", function( assert ) { 123 | let done = assert.async(); 124 | testUtils.generateTar().then((tar) => { 125 | let imageBlob = tar.getFileBlob("myfolder/tux.png", "image/png"); 126 | let imageURL = URL.createObjectURL(imageBlob); 127 | let image = new Image(); 128 | image.onload = (event) => { 129 | assert.equal(image.width, 265, "Image width is ok"); 130 | assert.equal(image.height, 314, "Image height is ok"); 131 | done(); 132 | }; 133 | image.src = imageURL; 134 | }); 135 | }); 136 | 137 | QUnit.test( "Download test", function( assert ) { 138 | let done = assert.async(); 139 | testUtils.generateTar(true).then((tar) => { 140 | assert.ok(1, "download test completed, please check tar file manually"); 141 | done(); 142 | }); 143 | }); 144 | 145 | 146 | --------------------------------------------------------------------------------