tarball.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. let tarball = {};
  2. tarball.TarReader = class {
  3. constructor() {
  4. this.fileInfo = [];
  5. }
  6. readFile(file) {
  7. return new Promise((resolve, reject) => {
  8. let reader = new FileReader();
  9. reader.onload = (event) => {
  10. this.buffer = event.target.result;
  11. this.fileInfo = [];
  12. this._readFileInfo();
  13. resolve(this.fileInfo);
  14. };
  15. reader.readAsArrayBuffer(file);
  16. });
  17. }
  18. readArrayBuffer(arrayBuffer) {
  19. return new Promise((resolve, reject) => {
  20. this.buffer = arrayBuffer;
  21. this.fileInfo = [];
  22. this._readFileInfo();
  23. resolve(this.fileInfo);
  24. });
  25. }
  26. _readFileInfo() {
  27. this.fileInfo = [];
  28. let offset = 0;
  29. let file_size = 0;
  30. let file_name = "";
  31. let file_type = null;
  32. while(offset < this.buffer.byteLength - 512) {
  33. file_name = this._readFileName(offset); // file name
  34. if(file_name.length == 0) {
  35. break;
  36. }
  37. file_type = this._readFileType(offset);
  38. file_size = this._readFileSize(offset);
  39. this.fileInfo.push({
  40. "name": file_name,
  41. "type": file_type,
  42. "size": file_size,
  43. "header_offset": offset
  44. });
  45. offset += (512 + 512*Math.trunc(file_size/512));
  46. if(file_size % 512) {
  47. offset += 512;
  48. }
  49. }
  50. }
  51. getFileInfo() {
  52. return this.fileInfo;
  53. }
  54. _readString(str_offset, size) {
  55. let strView = new Uint8Array(this.buffer, str_offset, size);
  56. let i = 0;
  57. let rtnStr = "";
  58. while(strView[i] != 0) {
  59. rtnStr += String.fromCharCode(strView[i]);
  60. i++;
  61. }
  62. return rtnStr;
  63. }
  64. _readFileName(header_offset) {
  65. let name = this._readString(header_offset, 100);
  66. return name;
  67. }
  68. _readFileType(header_offset) {
  69. // offset: 156
  70. let typeView = new Uint8Array(this.buffer, header_offset+156, 1);
  71. let typeStr = String.fromCharCode(typeView[0]);
  72. if(typeStr == "0") {
  73. return "file";
  74. } else if(typeStr == "5") {
  75. return "directory";
  76. } else {
  77. return typeStr;
  78. }
  79. }
  80. _readFileSize(header_offset) {
  81. // offset: 124
  82. let szView = new Uint8Array(this.buffer, header_offset+124, 12);
  83. let szStr = "";
  84. for(let i = 0; i < 11; i++) {
  85. szStr += String.fromCharCode(szView[i]);
  86. }
  87. return parseInt(szStr,8);
  88. }
  89. _readFileBlob(file_offset, size, mimetype) {
  90. let view = new Uint8Array(this.buffer, file_offset, size);
  91. let blob = new Blob([view], {"type": mimetype});
  92. return blob;
  93. }
  94. _readTextFile(file_offset, size) {
  95. let view = new Uint8Array(this.buffer, file_offset, size);
  96. var utf8 = Array.from(view).map(function (item) {
  97. return String.fromCharCode(item);
  98. }).join('');
  99. return decodeURIComponent(escape(utf8));
  100. }
  101. _readBase64File(file_offset, size) {
  102. let view = new Uint8Array(this.buffer, file_offset, size);
  103. //var b64encoded = btoa(String.fromCharCode.apply(null, view));
  104. return view;
  105. }
  106. getTextFile(file_name) {
  107. let i = this.fileInfo.findIndex(info => info.name == file_name);
  108. if(i >= 0) {
  109. let info = this.fileInfo[i];
  110. return this._readTextFile(info.header_offset+512, info.size);
  111. }
  112. }
  113. getFileBlob(file_name, mimetype) {
  114. let i = this.fileInfo.findIndex(info => info.name == file_name);
  115. if(i >= 0) {
  116. let info = this.fileInfo[i];
  117. return this._readFileBlob(info.header_offset+512, info.size, mimetype);
  118. }
  119. }
  120. decode(input) {
  121. var chr1, chr2, chr3;
  122. var enc1, enc2, enc3, enc4;
  123. var i = 0, resultIndex = 0;
  124. var dataUrlPrefix = "data:";
  125. if (input.substr(0, dataUrlPrefix.length) === dataUrlPrefix) {
  126. // This is a common error: people give a data url
  127. // (data:image/png;base64,iVBOR...) with a {base64: true} and
  128. // wonders why things don't work.
  129. // We can detect that the string input looks like a data url but we
  130. // *can't* be sure it is one: removing everything up to the comma would
  131. // be too dangerous.
  132. throw new Error("Invalid base64 input, it looks like a data url.");
  133. }
  134. input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
  135. var _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  136. var totalLength = input.length * 3 / 4;
  137. if(input.charAt(input.length - 1) === _keyStr.charAt(64)) {
  138. totalLength--;
  139. }
  140. if(input.charAt(input.length - 2) === _keyStr.charAt(64)) {
  141. totalLength--;
  142. }
  143. if (totalLength % 1 !== 0) {
  144. // totalLength is not an integer, the length does not match a valid
  145. // base64 content. That can happen if:
  146. // - the input is not a base64 content
  147. // - the input is *almost* a base64 content, with a extra chars at the
  148. // beginning or at the end
  149. // - the input uses a base64 variant (base64url for example)
  150. throw new Error("Invalid base64 input, bad content length.");
  151. }
  152. var output;
  153. output = new Uint8Array(totalLength|0);
  154. while (i < input.length) {
  155. enc1 = _keyStr.indexOf(input.charAt(i++));
  156. enc2 = _keyStr.indexOf(input.charAt(i++));
  157. enc3 = _keyStr.indexOf(input.charAt(i++));
  158. enc4 = _keyStr.indexOf(input.charAt(i++));
  159. chr1 = (enc1 << 2) | (enc2 >> 4);
  160. chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
  161. chr3 = ((enc3 & 3) << 6) | enc4;
  162. output[resultIndex++] = chr1;
  163. if (enc3 !== 64) {
  164. output[resultIndex++] = chr2;
  165. }
  166. if (enc4 !== 64) {
  167. output[resultIndex++] = chr3;
  168. }
  169. }
  170. return output;
  171. };
  172. };
  173. tarball.TarWriter = class {
  174. constructor() {
  175. this.fileData = [];
  176. }
  177. addTextFile(name, text, opts) {
  178. let buf = new ArrayBuffer(text.length);
  179. let arr = new Uint8Array(buf);
  180. for(let i = 0; i < text.length; i++) {
  181. arr[i] = text.charCodeAt(i);
  182. }
  183. this.fileData.push({
  184. name: name,
  185. array: arr,
  186. type: "file",
  187. size: arr.length,
  188. dataType: "array",
  189. opts: opts
  190. });
  191. }
  192. addFileArrayBuffer(name, arrayBuffer, opts) {
  193. let arr = new Uint8Array(arrayBuffer);
  194. this.fileData.push({
  195. name: name,
  196. array: arr,
  197. type: "file",
  198. size: arr.length,
  199. dataType: "array",
  200. opts: opts
  201. });
  202. }
  203. addFileUint8(name, arr, opts) {
  204. this.fileData.push({
  205. name: name,
  206. array: arr,
  207. type: "file",
  208. size: arr.length,
  209. dataType: "array",
  210. opts: opts
  211. });
  212. }
  213. addFile(name, file, opts) {
  214. this.fileData.push({
  215. name: name,
  216. file: file,
  217. size: file.size,
  218. type: "file",
  219. dataType: "file",
  220. opts: opts
  221. });
  222. }
  223. addFolder(name, opts) {
  224. this.fileData.push({
  225. name: name,
  226. type: "directory",
  227. size: 0,
  228. dataType: "none",
  229. opts: opts
  230. });
  231. }
  232. _createBuffer() {
  233. let tarDataSize = 0;
  234. for(let i = 0; i < this.fileData.length; i++) {
  235. let size = this.fileData[i].size;
  236. tarDataSize += 512 + 512*Math.trunc(size/512);
  237. if(size % 512) {
  238. tarDataSize += 512;
  239. }
  240. }
  241. let bufSize = 10240*Math.trunc(tarDataSize/10240);
  242. if(tarDataSize % 10240) {
  243. bufSize += 10240;
  244. }
  245. this.buffer = new ArrayBuffer(bufSize);
  246. }
  247. async download(filename) {
  248. let blob = await this.write();
  249. let $downloadElem = document.createElement('a');
  250. $downloadElem.href = URL.createObjectURL(blob);
  251. $downloadElem.download = filename;
  252. $downloadElem.style.display = "none";
  253. document.body.appendChild($downloadElem);
  254. $downloadElem.click();
  255. document.body.removeChild($downloadElem);
  256. }
  257. write() {
  258. return new Promise((resolve,reject) => {
  259. this._createBuffer();
  260. let offset = 0;
  261. let filesAdded = 0;
  262. let onFileDataAdded = () => {
  263. filesAdded++;
  264. if(filesAdded === this.fileData.length) {
  265. let arr = new Uint8Array(this.buffer);
  266. let blob = new Blob([arr], {"type":"application/x-tar"});
  267. resolve(blob);
  268. }
  269. };
  270. for(let fileIdx = 0; fileIdx < this.fileData.length; fileIdx++) {
  271. let fdata = this.fileData[fileIdx];
  272. // write header
  273. this._writeFileName(fdata.name, offset);
  274. this._writeFileType(fdata.type, offset);
  275. this._writeFileSize(fdata.size, offset);
  276. this._fillHeader(offset, fdata.opts, fdata.type);
  277. this._writeChecksum(offset);
  278. // write file data
  279. let destArray = new Uint8Array(this.buffer, offset+512, fdata.size);
  280. if(fdata.dataType === "array") {
  281. for(let byteIdx = 0; byteIdx < fdata.size; byteIdx++) {
  282. destArray[byteIdx] = fdata.array[byteIdx];
  283. }
  284. onFileDataAdded();
  285. } else if(fdata.dataType === "file") {
  286. let reader = new FileReader();
  287. reader.onload = (function(outArray) {
  288. let dArray = outArray;
  289. return function(event) {
  290. let sbuf = event.target.result;
  291. let sarr = new Uint8Array(sbuf);
  292. for(let bIdx = 0; bIdx < sarr.length; bIdx++) {
  293. dArray[bIdx] = sarr[bIdx];
  294. }
  295. onFileDataAdded();
  296. };
  297. })(destArray);
  298. reader.readAsArrayBuffer(fdata.file);
  299. } else if(fdata.type === "directory") {
  300. onFileDataAdded();
  301. }
  302. offset += (512 + 512*Math.trunc(fdata.size/512));
  303. if(fdata.size % 512) {
  304. offset += 512;
  305. }
  306. }
  307. });
  308. }
  309. _writeString(str, offset, size) {
  310. let strView = new Uint8Array(this.buffer, offset, size);
  311. for(let i = 0; i < size; i++) {
  312. if(i < str.length) {
  313. strView[i] = str.charCodeAt(i);
  314. } else {
  315. strView[i] = 0;
  316. }
  317. }
  318. }
  319. _writeFileName(name, header_offset) {
  320. // offset: 0
  321. this._writeString(name, header_offset, 100);
  322. }
  323. _writeFileType(typeStr, header_offset) {
  324. // offset: 156
  325. let typeChar = "0";
  326. if(typeStr === "file") {
  327. typeChar = "0";
  328. } else if(typeStr === "directory") {
  329. typeChar = "5";
  330. }
  331. let typeView = new Uint8Array(this.buffer, header_offset + 156, 1);
  332. typeView[0] = typeChar.charCodeAt(0);
  333. }
  334. _writeFileSize(size, header_offset) {
  335. // offset: 124
  336. let sz = size.toString(8);
  337. sz = this._leftPad(sz, 11);
  338. this._writeString(sz, header_offset+124, 12);
  339. }
  340. _leftPad(number, targetLength) {
  341. let output = number + '';
  342. while (output.length < targetLength) {
  343. output = '0' + output;
  344. }
  345. return output;
  346. }
  347. _writeFileMode(mode, header_offset) {
  348. // offset: 100
  349. this._writeString(this._leftPad(mode,7), header_offset+100, 8);
  350. }
  351. _writeFileUid(uid, header_offset) {
  352. // offset: 108
  353. this._writeString(this._leftPad(uid,7), header_offset+108, 8);
  354. }
  355. _writeFileGid(gid, header_offset) {
  356. // offset: 116
  357. this._writeString(this._leftPad(gid,7), header_offset+116, 8);
  358. }
  359. _writeFileMtime(mtime, header_offset) {
  360. // offset: 136
  361. this._writeString(this._leftPad(mtime,11), header_offset+136, 12);
  362. }
  363. _writeFileUser(user, header_offset) {
  364. // offset: 265
  365. this._writeString(user, header_offset+265, 32);
  366. }
  367. _writeFileGroup(group, header_offset) {
  368. // offset: 297
  369. this._writeString(group, header_offset+297, 32);
  370. }
  371. _writeChecksum(header_offset) {
  372. // offset: 148
  373. this._writeString(" ", header_offset+148, 8); // first fill with spaces
  374. // add up header bytes
  375. let header = new Uint8Array(this.buffer, header_offset, 512);
  376. let chksum = 0;
  377. for(let i = 0; i < 512; i++) {
  378. chksum += header[i];
  379. }
  380. this._writeString(chksum.toString(8), header_offset+148, 8);
  381. }
  382. _getOpt(opts, opname, defaultVal) {
  383. if(opts != null) {
  384. if(opts[opname] != null) {
  385. return opts[opname];
  386. }
  387. }
  388. return defaultVal;
  389. }
  390. _fillHeader(header_offset, opts, fileType) {
  391. let uid = this._getOpt(opts, "uid", 1000);
  392. let gid = this._getOpt(opts, "gid", 1000);
  393. let mode = this._getOpt(opts, "mode", fileType === "file" ? "664" : "775");
  394. let mtime = this._getOpt(opts, "mtime", Date.now());
  395. let user = this._getOpt(opts, "user", "tarballjs");
  396. let group = this._getOpt(opts, "group", "tarballjs");
  397. this._writeFileMode(mode, header_offset);
  398. this._writeFileUid(uid.toString(8), header_offset);
  399. this._writeFileGid(gid.toString(8), header_offset);
  400. this._writeFileMtime(Math.trunc(mtime/1000).toString(8), header_offset);
  401. this._writeString("ustar", header_offset+257,6); // magic string
  402. this._writeString("00", header_offset+263,2); // magic version
  403. this._writeFileUser(user, header_offset);
  404. this._writeFileGroup(group, header_offset);
  405. }
  406. };