├── 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 |
--------------------------------------------------------------------------------