mirror of
https://github.com/coder/coder.git
synced 2026-11-03 05:55:24 +08:00
354 lines
8.6 KiB
TypeScript
354 lines
8.6 KiB
TypeScript
// Based on https://github.com/gera2ld/tarjs
|
|
// and https://github.com/ankitrohatgi/tarballjs/blob/master/tarball.js
|
|
type TarFileType = string;
|
|
// Added the most common codes
|
|
export const TarFileTypeCodes = {
|
|
File: "0",
|
|
Dir: "5",
|
|
};
|
|
const encoder = new TextEncoder();
|
|
const utf8Encode = (input: string) => encoder.encode(input);
|
|
const decoder = new TextDecoder();
|
|
const utf8Decode = (input: Uint8Array) => decoder.decode(input);
|
|
|
|
export interface ITarFileInfo {
|
|
name: string;
|
|
type: TarFileType;
|
|
size: number;
|
|
mode: number;
|
|
mtime: number;
|
|
user: string;
|
|
group: string;
|
|
headerOffset: number;
|
|
}
|
|
|
|
interface ITarWriteItem {
|
|
name: string;
|
|
type: TarFileType;
|
|
data: ArrayBuffer | Promise<ArrayBuffer> | null;
|
|
size: number;
|
|
opts?: Partial<ITarWriteOptions>;
|
|
}
|
|
|
|
interface ITarWriteOptions {
|
|
uid: number;
|
|
gid: number;
|
|
mode: number;
|
|
mtime: number;
|
|
user: string;
|
|
group: string;
|
|
}
|
|
|
|
export class TarReader {
|
|
public fileInfo: ITarFileInfo[] = [];
|
|
private _buffer: ArrayBuffer | null = null;
|
|
|
|
constructor() {
|
|
this.reset();
|
|
}
|
|
|
|
get buffer() {
|
|
if (!this._buffer) {
|
|
throw new Error("Buffer is not set");
|
|
}
|
|
|
|
return this._buffer;
|
|
}
|
|
|
|
reset() {
|
|
this.fileInfo = [];
|
|
this._buffer = null;
|
|
}
|
|
|
|
async readFile(file: ArrayBuffer | Uint8Array | Blob) {
|
|
this.reset();
|
|
this._buffer = await getArrayBuffer(file);
|
|
this.readFileInfo();
|
|
return this.fileInfo;
|
|
}
|
|
|
|
private readFileInfo() {
|
|
this.fileInfo = [];
|
|
let offset = 0;
|
|
|
|
while (offset < this.buffer.byteLength - 512) {
|
|
const fileName = this.readFileName(offset);
|
|
if (!fileName) {
|
|
break;
|
|
}
|
|
const fileType = this.readFileType(offset);
|
|
const fileSize = this.readFileSize(offset);
|
|
const fileMode = this.readFileMode(offset);
|
|
const fileMtime = this.readFileMtime(offset);
|
|
const fileUser = this.readFileUser(offset);
|
|
const fileGroup = this.readFileGroup(offset);
|
|
|
|
this.fileInfo.push({
|
|
name: fileName,
|
|
type: fileType,
|
|
size: fileSize,
|
|
headerOffset: offset,
|
|
mode: fileMode,
|
|
mtime: fileMtime,
|
|
user: fileUser,
|
|
group: fileGroup,
|
|
});
|
|
|
|
offset += 512 + 512 * Math.floor((fileSize + 511) / 512);
|
|
}
|
|
}
|
|
|
|
private readString(offset: number, maxSize: number) {
|
|
let size = 0;
|
|
let view = new Uint8Array(this.buffer, offset, maxSize);
|
|
while (size < maxSize && view[size]) {
|
|
size += 1;
|
|
}
|
|
view = new Uint8Array(this.buffer, offset, size);
|
|
return utf8Decode(view);
|
|
}
|
|
|
|
private readFileName(offset: number) {
|
|
return this.readString(offset, 100);
|
|
}
|
|
|
|
private readFileMode(offset: number) {
|
|
const mode = this.readString(offset + 100, 8);
|
|
return Number.parseInt(mode, 8);
|
|
}
|
|
|
|
private readFileMtime(offset: number) {
|
|
const mtime = this.readString(offset + 136, 12);
|
|
return Number.parseInt(mtime, 8);
|
|
}
|
|
|
|
private readFileUser(offset: number) {
|
|
return this.readString(offset + 265, 32);
|
|
}
|
|
|
|
private readFileGroup(offset: number) {
|
|
return this.readString(offset + 297, 32);
|
|
}
|
|
|
|
private readFileType(offset: number) {
|
|
const typeView = new Uint8Array(this.buffer, offset + 156, 1);
|
|
const typeStr = String.fromCharCode(typeView[0]);
|
|
return typeStr;
|
|
}
|
|
|
|
private readFileSize(offset: number) {
|
|
// offset = 124, length = 12
|
|
const view = new Uint8Array(this.buffer, offset + 124, 12);
|
|
const sizeStr = utf8Decode(view);
|
|
return Number.parseInt(sizeStr, 8);
|
|
}
|
|
|
|
private readFileBlob(offset: number, size: number, mimetype: string) {
|
|
const view = new Uint8Array(this.buffer, offset, size);
|
|
return new Blob([view], { type: mimetype });
|
|
}
|
|
|
|
private readTextFile(offset: number, size: number) {
|
|
const view = new Uint8Array(this.buffer, offset, size);
|
|
return utf8Decode(view);
|
|
}
|
|
|
|
getTextFile(filename: string) {
|
|
const item = this.fileInfo.find((info) => info.name === filename);
|
|
if (item) {
|
|
return this.readTextFile(item.headerOffset + 512, item.size);
|
|
}
|
|
}
|
|
|
|
getFileBlob(filename: string, mimetype = "") {
|
|
const item = this.fileInfo.find((info) => info.name === filename);
|
|
if (item) {
|
|
return this.readFileBlob(item.headerOffset + 512, item.size, mimetype);
|
|
}
|
|
}
|
|
}
|
|
|
|
export class TarWriter {
|
|
private fileData: ITarWriteItem[] = [];
|
|
private _buffer: ArrayBuffer | null = null;
|
|
|
|
get buffer() {
|
|
if (!this._buffer) {
|
|
throw new Error("Buffer is not set");
|
|
}
|
|
return this._buffer;
|
|
}
|
|
|
|
addFile(
|
|
name: string,
|
|
file: string | ArrayBuffer | Uint8Array | Blob,
|
|
opts?: Partial<ITarWriteOptions>,
|
|
) {
|
|
const data = getArrayBuffer(file);
|
|
const size = (data as ArrayBuffer).byteLength ?? (file as Blob).size;
|
|
const item: ITarWriteItem = {
|
|
name,
|
|
type: TarFileTypeCodes.File,
|
|
data,
|
|
size,
|
|
opts,
|
|
};
|
|
this.fileData.push(item);
|
|
}
|
|
|
|
addFolder(name: string, opts?: Partial<ITarWriteOptions>) {
|
|
this.fileData.push({
|
|
name,
|
|
type: TarFileTypeCodes.Dir,
|
|
data: null,
|
|
size: 0,
|
|
opts,
|
|
});
|
|
}
|
|
|
|
private createBuffer() {
|
|
const dataSize = this.fileData.reduce(
|
|
(prev, item) => prev + 512 + 512 * Math.floor((item.size + 511) / 512),
|
|
0,
|
|
);
|
|
const bufSize = 10240 * Math.floor((dataSize + 10240 - 1) / 10240);
|
|
this._buffer = new ArrayBuffer(bufSize);
|
|
}
|
|
|
|
async write() {
|
|
this.createBuffer();
|
|
const view = new Uint8Array(this.buffer);
|
|
let offset = 0;
|
|
for (const item of this.fileData) {
|
|
// write header
|
|
this.writeFileName(item.name, offset);
|
|
this.writeFileType(item.type, offset);
|
|
this.writeFileSize(item.size, offset);
|
|
this.fillHeader(
|
|
offset,
|
|
item.opts as Partial<ITarWriteOptions>,
|
|
item.type,
|
|
);
|
|
this.writeChecksum(offset);
|
|
|
|
// write data
|
|
const data = new Uint8Array((await item.data) as ArrayBuffer);
|
|
view.set(data, offset + 512);
|
|
offset += 512 + 512 * Math.floor((item.size + 511) / 512);
|
|
}
|
|
// Required so it works in the browser and node.
|
|
if (typeof Blob !== "undefined") {
|
|
return new Blob([this.buffer], { type: "application/x-tar" });
|
|
}
|
|
return this.buffer;
|
|
}
|
|
|
|
private writeString(str: string, offset: number, size: number) {
|
|
const strView = utf8Encode(str);
|
|
const view = new Uint8Array(this.buffer, offset, size);
|
|
for (let i = 0; i < size; i += 1) {
|
|
view[i] = i < strView.length ? strView[i] : 0;
|
|
}
|
|
}
|
|
|
|
private writeFileName(name: string, offset: number) {
|
|
// offset: 0
|
|
this.writeString(name, offset, 100);
|
|
}
|
|
|
|
private writeFileType(type: TarFileType, offset: number) {
|
|
// offset: 156
|
|
const typeView = new Uint8Array(this.buffer, offset + 156, 1);
|
|
typeView[0] = type.charCodeAt(0);
|
|
}
|
|
|
|
private writeFileSize(size: number, offset: number) {
|
|
// offset: 124
|
|
const sizeStr = size.toString(8).padStart(11, "0");
|
|
this.writeString(sizeStr, offset + 124, 12);
|
|
}
|
|
|
|
private writeFileMode(mode: number, offset: number) {
|
|
// offset: 100
|
|
this.writeString(mode.toString(8).padStart(7, "0"), offset + 100, 8);
|
|
}
|
|
|
|
private writeFileUid(uid: number, offset: number) {
|
|
// offset: 108
|
|
this.writeString(uid.toString(8).padStart(7, "0"), offset + 108, 8);
|
|
}
|
|
|
|
private writeFileGid(gid: number, offset: number) {
|
|
// offset: 116
|
|
this.writeString(gid.toString(8).padStart(7, "0"), offset + 116, 8);
|
|
}
|
|
|
|
private writeFileMtime(mtime: number, offset: number) {
|
|
// offset: 136
|
|
this.writeString(mtime.toString(8).padStart(11, "0"), offset + 136, 12);
|
|
}
|
|
|
|
private writeFileUser(user: string, offset: number) {
|
|
// offset: 265
|
|
this.writeString(user, offset + 265, 32);
|
|
}
|
|
|
|
private writeFileGroup(group: string, offset: number) {
|
|
// offset: 297
|
|
this.writeString(group, offset + 297, 32);
|
|
}
|
|
|
|
private writeChecksum(offset: number) {
|
|
// offset: 148
|
|
this.writeString(" ", offset + 148, 8); // first fill with spaces
|
|
|
|
// add up header bytes
|
|
const header = new Uint8Array(this.buffer, offset, 512);
|
|
let chksum = 0;
|
|
for (let i = 0; i < 512; i += 1) {
|
|
chksum += header[i];
|
|
}
|
|
this.writeString(chksum.toString(8), offset + 148, 8);
|
|
}
|
|
|
|
private fillHeader(
|
|
offset: number,
|
|
opts: Partial<ITarWriteOptions>,
|
|
fileType: TarFileType,
|
|
) {
|
|
const { uid, gid, mode, mtime, user, group } = {
|
|
uid: 1000,
|
|
gid: 1000,
|
|
mode: fileType === TarFileTypeCodes.File ? 0o664 : 0o775,
|
|
mtime: Math.trunc(Date.now() / 1000),
|
|
user: "tarballjs",
|
|
group: "tarballjs",
|
|
...opts,
|
|
};
|
|
|
|
this.writeFileMode(mode, offset);
|
|
this.writeFileUid(uid, offset);
|
|
this.writeFileGid(gid, offset);
|
|
this.writeFileMtime(mtime, offset);
|
|
|
|
this.writeString("ustar", offset + 257, 6); // magic string
|
|
this.writeString("00", offset + 263, 2); // magic version
|
|
|
|
this.writeFileUser(user, offset);
|
|
this.writeFileGroup(group, offset);
|
|
}
|
|
}
|
|
|
|
function getArrayBuffer(file: string | ArrayBuffer | Uint8Array | Blob) {
|
|
if (typeof file === "string") {
|
|
return utf8Encode(file).buffer;
|
|
}
|
|
if (file instanceof ArrayBuffer) {
|
|
return file;
|
|
}
|
|
if (ArrayBuffer.isView(file)) {
|
|
return new Uint8Array(file).buffer;
|
|
}
|
|
return file.arrayBuffer();
|
|
}
|