mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): Upload template files on template version editor (#6222)
This commit is contained in:
@@ -3,6 +3,14 @@ import { cleanup } from "@testing-library/react"
|
|||||||
import crypto from "crypto"
|
import crypto from "crypto"
|
||||||
import { server } from "./src/testHelpers/server"
|
import { server } from "./src/testHelpers/server"
|
||||||
import "jest-location-mock"
|
import "jest-location-mock"
|
||||||
|
import { TextEncoder, TextDecoder } from "util"
|
||||||
|
import { Blob } from "buffer"
|
||||||
|
|
||||||
|
global.TextEncoder = TextEncoder
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
|
||||||
|
global.TextDecoder = TextDecoder as any
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
|
||||||
|
global.Blob = Blob as any
|
||||||
|
|
||||||
// Polyfill the getRandomValues that is used on utils/random.ts
|
// Polyfill the getRandomValues that is used on utils/random.ts
|
||||||
Object.defineProperty(global.self, "crypto", {
|
Object.defineProperty(global.self, "crypto", {
|
||||||
|
|||||||
Vendored
+10
-3
@@ -1,14 +1,21 @@
|
|||||||
declare module "js-untar" {
|
declare module "js-untar" {
|
||||||
interface File {
|
export interface File {
|
||||||
name: string
|
name: string
|
||||||
|
mode: string
|
||||||
blob: Blob
|
blob: Blob
|
||||||
|
gid: number
|
||||||
|
uid: number
|
||||||
|
mtime: number
|
||||||
|
gname: string
|
||||||
|
uname: string
|
||||||
|
type: "0" | "1" | "2" | "3" | "4" | "5" //https://en.wikipedia.org/wiki/Tar_(computing) on Type flag field
|
||||||
}
|
}
|
||||||
|
|
||||||
const Untar: (buffer: ArrayBuffer) => {
|
const Untar: (buffer: ArrayBuffer) => {
|
||||||
then: (
|
then: (
|
||||||
resolve?: () => Promise<void>,
|
resolve?: (files: File[]) => void,
|
||||||
reject?: () => Promise<void>,
|
reject?: () => Promise<void>,
|
||||||
progress: (file: File) => Promise<void>,
|
progress?: (file: File) => Promise<void>,
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,6 @@
|
|||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
"rollup-plugin-visualizer": "5.9.0",
|
"rollup-plugin-visualizer": "5.9.0",
|
||||||
"sourcemapped-stacktrace": "1.1.11",
|
"sourcemapped-stacktrace": "1.1.11",
|
||||||
"tar-js": "^0.3.0",
|
|
||||||
"ts-prune": "0.10.3",
|
"ts-prune": "0.10.3",
|
||||||
"tzdata": "1.0.30",
|
"tzdata": "1.0.30",
|
||||||
"ua-parser-js": "1.0.33",
|
"ua-parser-js": "1.0.33",
|
||||||
|
|||||||
+13
-6
@@ -16,14 +16,21 @@ type Params = {
|
|||||||
export const TemplateVersionEditorPage: FC = () => {
|
export const TemplateVersionEditorPage: FC = () => {
|
||||||
const { version: versionName, template: templateName } = useParams() as Params
|
const { version: versionName, template: templateName } = useParams() as Params
|
||||||
const orgId = useOrganizationId()
|
const orgId = useOrganizationId()
|
||||||
const { isSuccess, data } = useTemplateVersionData(
|
|
||||||
orgId,
|
|
||||||
templateName,
|
|
||||||
versionName,
|
|
||||||
)
|
|
||||||
const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, {
|
const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, {
|
||||||
context: { orgId },
|
context: { orgId },
|
||||||
})
|
})
|
||||||
|
const { isSuccess, data } = useTemplateVersionData(
|
||||||
|
{
|
||||||
|
orgId,
|
||||||
|
templateName,
|
||||||
|
versionName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
sendEvent({ type: "INITIALIZE", untarFiles: data.untarFiles })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -34,7 +41,7 @@ export const TemplateVersionEditorPage: FC = () => {
|
|||||||
{isSuccess && (
|
{isSuccess && (
|
||||||
<TemplateVersionEditor
|
<TemplateVersionEditor
|
||||||
template={data.template}
|
template={data.template}
|
||||||
templateVersion={editorState.context.version || data.currentVersion}
|
templateVersion={editorState.context.version || data.version}
|
||||||
defaultFileTree={data.fileTree}
|
defaultFileTree={data.fileTree}
|
||||||
onPreview={(fileTree) => {
|
onPreview={(fileTree) => {
|
||||||
sendEvent({
|
sendEvent({
|
||||||
|
|||||||
@@ -1,32 +1,49 @@
|
|||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery, UseQueryOptions } from "@tanstack/react-query"
|
||||||
import { getTemplateByName, getTemplateVersionByName } from "api/api"
|
import { getFile, getTemplateByName, getTemplateVersionByName } from "api/api"
|
||||||
import { createTemplateVersionFileTree } from "util/templateVersion"
|
import { createTemplateVersionFileTree } from "util/templateVersion"
|
||||||
|
import untar, { File as UntarFile } from "js-untar"
|
||||||
|
|
||||||
const getTemplateVersionData = async (
|
const getTemplateVersionData = async (
|
||||||
orgId: string,
|
orgId: string,
|
||||||
templateName: string,
|
templateName: string,
|
||||||
versionName: string,
|
versionName: string,
|
||||||
) => {
|
) => {
|
||||||
const [template, currentVersion] = await Promise.all([
|
const [template, version] = await Promise.all([
|
||||||
getTemplateByName(orgId, templateName),
|
getTemplateByName(orgId, templateName),
|
||||||
getTemplateVersionByName(orgId, templateName, versionName),
|
getTemplateVersionByName(orgId, templateName, versionName),
|
||||||
])
|
])
|
||||||
const fileTree = await createTemplateVersionFileTree(currentVersion)
|
const tarFile = await getFile(version.job.file_id)
|
||||||
|
let untarFiles: UntarFile[] = []
|
||||||
|
await untar(tarFile).then((files) => {
|
||||||
|
untarFiles = files
|
||||||
|
})
|
||||||
|
const fileTree = await createTemplateVersionFileTree(untarFiles)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
template,
|
template,
|
||||||
currentVersion,
|
version,
|
||||||
fileTree,
|
fileTree,
|
||||||
|
untarFiles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetTemplateVersionResponse = Awaited<
|
||||||
|
ReturnType<typeof getTemplateVersionData>
|
||||||
|
>
|
||||||
|
|
||||||
|
type UseTemplateVersionDataParams = {
|
||||||
|
orgId: string
|
||||||
|
templateName: string
|
||||||
|
versionName: string
|
||||||
|
}
|
||||||
|
|
||||||
export const useTemplateVersionData = (
|
export const useTemplateVersionData = (
|
||||||
orgId: string,
|
{ templateName, versionName, orgId }: UseTemplateVersionDataParams,
|
||||||
templateName: string,
|
options?: UseQueryOptions<GetTemplateVersionResponse>,
|
||||||
versionName: string,
|
|
||||||
) => {
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["templateVersion", templateName, versionName],
|
queryKey: ["templateVersion", templateName, versionName],
|
||||||
queryFn: () => getTemplateVersionData(orgId, templateName, versionName),
|
queryFn: () => getTemplateVersionData(orgId, templateName, versionName),
|
||||||
|
...options,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { TarReader, TarWriter, ITarFileInfo, TarFileType } from "./tar"
|
||||||
|
|
||||||
|
const mtime = 1666666666666
|
||||||
|
|
||||||
|
test("tar", async () => {
|
||||||
|
// Write
|
||||||
|
const writer = new TarWriter()
|
||||||
|
writer.addFile("a.txt", "hello", { mtime })
|
||||||
|
writer.addFile("b.txt", new Blob(["world"]), { mtime })
|
||||||
|
writer.addFile("c.txt", "", { mtime })
|
||||||
|
writer.addFolder("etc", { mtime })
|
||||||
|
writer.addFile("etc/d.txt", "", { mtime })
|
||||||
|
const blob = await writer.write()
|
||||||
|
|
||||||
|
// Read
|
||||||
|
const reader = new TarReader()
|
||||||
|
const fileInfos = await reader.readFile(blob)
|
||||||
|
verifyFile(fileInfos[0], reader.getTextFile(fileInfos[0].name) as string, {
|
||||||
|
name: "a.txt",
|
||||||
|
content: "hello",
|
||||||
|
})
|
||||||
|
verifyFile(fileInfos[1], reader.getTextFile(fileInfos[1].name) as string, {
|
||||||
|
name: "b.txt",
|
||||||
|
content: "world",
|
||||||
|
})
|
||||||
|
verifyFile(fileInfos[2], reader.getTextFile(fileInfos[2].name) as string, {
|
||||||
|
name: "c.txt",
|
||||||
|
content: "",
|
||||||
|
})
|
||||||
|
verifyFolder(fileInfos[3], {
|
||||||
|
name: "etc",
|
||||||
|
})
|
||||||
|
verifyFile(fileInfos[4], reader.getTextFile(fileInfos[4].name) as string, {
|
||||||
|
name: "etc/d.txt",
|
||||||
|
content: "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function verifyFile(
|
||||||
|
info: ITarFileInfo,
|
||||||
|
content: string,
|
||||||
|
expected: { name: string; content: string },
|
||||||
|
) {
|
||||||
|
expect(info.name).toEqual(expected.name)
|
||||||
|
expect(info.size).toEqual(expected.content.length)
|
||||||
|
expect(content).toEqual(expected.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyFolder(info: ITarFileInfo, expected: { name: string }) {
|
||||||
|
expect(info.name).toEqual(expected.name)
|
||||||
|
expect(info.type).toEqual(TarFileType.Dir)
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
// Based on https://github.com/gera2ld/tarjs
|
||||||
|
// and https://github.com/ankitrohatgi/tarballjs/blob/master/tarball.js
|
||||||
|
export enum TarFileType {
|
||||||
|
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
|
||||||
|
headerOffset: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITarWriteItem {
|
||||||
|
name: string
|
||||||
|
type: TarFileType
|
||||||
|
data: ArrayBuffer | Promise<ArrayBuffer> | null
|
||||||
|
size: number
|
||||||
|
opts?: Partial<ITarWriteOptions>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITarWriteOptions {
|
||||||
|
uid: number
|
||||||
|
gid: number
|
||||||
|
mode: number
|
||||||
|
mtime: number
|
||||||
|
user: string
|
||||||
|
group: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TarReader {
|
||||||
|
private 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
|
||||||
|
let fileSize = 0
|
||||||
|
let fileName = ""
|
||||||
|
let fileType: TarFileType
|
||||||
|
while (offset < this.buffer.byteLength - 512) {
|
||||||
|
fileName = this.readFileName(offset)
|
||||||
|
if (!fileName) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fileType = this.readFileType(offset)
|
||||||
|
fileSize = this.readFileSize(offset)
|
||||||
|
|
||||||
|
this.fileInfo.push({
|
||||||
|
name: fileName,
|
||||||
|
type: fileType,
|
||||||
|
size: fileSize,
|
||||||
|
headerOffset: offset,
|
||||||
|
})
|
||||||
|
|
||||||
|
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 readFileType(offset: number) {
|
||||||
|
const typeView = new Uint8Array(this.buffer, offset + 156, 1)
|
||||||
|
const typeStr = String.fromCharCode(typeView[0])
|
||||||
|
if (typeStr === "0") {
|
||||||
|
return TarFileType.File
|
||||||
|
} else if (typeStr === "5") {
|
||||||
|
return TarFileType.Dir
|
||||||
|
} else {
|
||||||
|
throw new Error("No supported file type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readFileSize(offset: number) {
|
||||||
|
// offset = 124, length = 12
|
||||||
|
const view = new Uint8Array(this.buffer, offset + 124, 12)
|
||||||
|
const sizeStr = utf8Decode(view)
|
||||||
|
return 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: TarFileType.File,
|
||||||
|
data,
|
||||||
|
size,
|
||||||
|
opts,
|
||||||
|
}
|
||||||
|
this.fileData.push(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
addFolder(name: string, opts?: Partial<ITarWriteOptions>) {
|
||||||
|
this.fileData.push({
|
||||||
|
name,
|
||||||
|
type: TarFileType.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)
|
||||||
|
}
|
||||||
|
return new Blob([this.buffer], { type: "application/x-tar" })
|
||||||
|
}
|
||||||
|
|
||||||
|
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 === TarFileType.File ? 0o664 : 0o775,
|
||||||
|
mtime: ~~(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()
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as API from "api/api"
|
import * as API from "api/api"
|
||||||
import { TemplateVersion } from "api/typesGenerated"
|
import { TemplateVersion } from "api/typesGenerated"
|
||||||
import untar from "js-untar"
|
import untar, { File as UntarFile } from "js-untar"
|
||||||
import { FileTree, setFile } from "./filetree"
|
import { FileTree, setFile } from "./filetree"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,21 +41,22 @@ export const getTemplateVersionFiles = async (
|
|||||||
|
|
||||||
const allowedExtensions = ["tf", "md", "Dockerfile"]
|
const allowedExtensions = ["tf", "md", "Dockerfile"]
|
||||||
|
|
||||||
|
export const isAllowedFile = (name: string) => {
|
||||||
|
return allowedExtensions.some((ext) => name.endsWith(ext))
|
||||||
|
}
|
||||||
|
|
||||||
export const createTemplateVersionFileTree = async (
|
export const createTemplateVersionFileTree = async (
|
||||||
version: TemplateVersion,
|
untarFiles: UntarFile[],
|
||||||
): Promise<FileTree> => {
|
): Promise<FileTree> => {
|
||||||
let fileTree: FileTree = {}
|
let fileTree: FileTree = {}
|
||||||
const tarFile = await API.getFile(version.job.file_id)
|
|
||||||
const blobs: Record<string, Blob> = {}
|
const blobs: Record<string, Blob> = {}
|
||||||
|
|
||||||
await untar(tarFile).then(undefined, undefined, async (file) => {
|
for (const untarFile of untarFiles) {
|
||||||
if (allowedExtensions.some((ext) => file.name.endsWith(ext))) {
|
if (isAllowedFile(untarFile.name)) {
|
||||||
blobs[file.name] = file.blob
|
blobs[untarFile.name] = untarFile.blob
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
// We don't want to get the blob text during untar to not block the main thread.
|
|
||||||
// Also, by doing it here, we can make all the loading in parallel.
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.entries(blobs).map(async ([fullPath, blob]) => {
|
Object.entries(blobs).map(async ([fullPath, blob]) => {
|
||||||
const content = await blob.text()
|
const content = await blob.text()
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import {
|
|||||||
} from "api/typesGenerated"
|
} from "api/typesGenerated"
|
||||||
import { assign, createMachine } from "xstate"
|
import { assign, createMachine } from "xstate"
|
||||||
import * as API from "api/api"
|
import * as API from "api/api"
|
||||||
import Tar from "tar-js"
|
import { File as UntarFile } from "js-untar"
|
||||||
import { FileTree, traverse } from "util/filetree"
|
import { FileTree, traverse } from "util/filetree"
|
||||||
|
import { isAllowedFile } from "util/templateVersion"
|
||||||
|
import { TarWriter } from "util/tar"
|
||||||
|
|
||||||
export interface CreateVersionData {
|
export interface CreateVersionData {
|
||||||
file: File
|
file: File
|
||||||
@@ -22,6 +24,7 @@ export interface TemplateVersionEditorMachineContext {
|
|||||||
version?: TemplateVersion
|
version?: TemplateVersion
|
||||||
resources?: WorkspaceResource[]
|
resources?: WorkspaceResource[]
|
||||||
buildLogs?: ProvisionerJobLog[]
|
buildLogs?: ProvisionerJobLog[]
|
||||||
|
untarFiles?: UntarFile[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const templateVersionEditorMachine = createMachine(
|
export const templateVersionEditorMachine = createMachine(
|
||||||
@@ -31,6 +34,7 @@ export const templateVersionEditorMachine = createMachine(
|
|||||||
schema: {
|
schema: {
|
||||||
context: {} as TemplateVersionEditorMachineContext,
|
context: {} as TemplateVersionEditorMachineContext,
|
||||||
events: {} as
|
events: {} as
|
||||||
|
| { type: "INITIALIZE"; untarFiles: UntarFile[] }
|
||||||
| {
|
| {
|
||||||
type: "CREATE_VERSION"
|
type: "CREATE_VERSION"
|
||||||
fileTree: FileTree
|
fileTree: FileTree
|
||||||
@@ -61,8 +65,16 @@ export const templateVersionEditorMachine = createMachine(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
tsTypes: {} as import("./templateVersionEditorXService.typegen").Typegen0,
|
tsTypes: {} as import("./templateVersionEditorXService.typegen").Typegen0,
|
||||||
initial: "idle",
|
initial: "initializing",
|
||||||
states: {
|
states: {
|
||||||
|
initializing: {
|
||||||
|
on: {
|
||||||
|
INITIALIZE: {
|
||||||
|
actions: ["assignUntarFiles"],
|
||||||
|
target: "idle",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
idle: {
|
idle: {
|
||||||
on: {
|
on: {
|
||||||
CREATE_VERSION: {
|
CREATE_VERSION: {
|
||||||
@@ -201,20 +213,51 @@ export const templateVersionEditorMachine = createMachine(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
assignUntarFiles: assign({
|
||||||
|
untarFiles: (_, { untarFiles }) => untarFiles,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
uploadTar: (ctx) => {
|
uploadTar: async ({ fileTree, untarFiles }) => {
|
||||||
if (!ctx.fileTree) {
|
if (!fileTree) {
|
||||||
throw new Error("files must be set")
|
throw new Error("file tree must to be set")
|
||||||
}
|
}
|
||||||
const tar = new Tar()
|
if (!untarFiles) {
|
||||||
let out: Uint8Array = new Uint8Array()
|
throw new Error("untar files must to be set")
|
||||||
traverse(ctx.fileTree, (content, _filename, fullPath) => {
|
}
|
||||||
|
const tar = new TarWriter()
|
||||||
|
|
||||||
|
// Add previous non editable files
|
||||||
|
for (const untarFile of untarFiles) {
|
||||||
|
if (!isAllowedFile(untarFile.name)) {
|
||||||
|
if (untarFile.type === "5") {
|
||||||
|
tar.addFolder(untarFile.name, {
|
||||||
|
mode: parseInt(untarFile.mode, 8) & 0xfff, // https://github.com/beatgammit/tar-js/blob/master/lib/tar.js#L42
|
||||||
|
mtime: untarFile.mtime,
|
||||||
|
user: untarFile.uname,
|
||||||
|
group: untarFile.gname,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const buffer = await untarFile.blob.arrayBuffer()
|
||||||
|
tar.addFile(untarFile.name, new Uint8Array(buffer), {
|
||||||
|
mode: parseInt(untarFile.mode, 8) & 0xfff, // https://github.com/beatgammit/tar-js/blob/master/lib/tar.js#L42
|
||||||
|
mtime: untarFile.mtime,
|
||||||
|
user: untarFile.uname,
|
||||||
|
group: untarFile.gname,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the editable files
|
||||||
|
traverse(fileTree, (content, _filename, fullPath) => {
|
||||||
if (typeof content === "string") {
|
if (typeof content === "string") {
|
||||||
out = tar.append(fullPath, content)
|
tar.addFile(fullPath, content)
|
||||||
|
} else {
|
||||||
|
tar.addFolder(fullPath)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return API.uploadTemplateFile(new File([out], "template.tar"))
|
const blob = await tar.write()
|
||||||
|
return API.uploadTemplateFile(new File([blob], "template.tar"))
|
||||||
},
|
},
|
||||||
createBuild: (ctx) => {
|
createBuild: (ctx) => {
|
||||||
if (!ctx.uploadResponse) {
|
if (!ctx.uploadResponse) {
|
||||||
|
|||||||
@@ -13678,11 +13678,6 @@ tapable@^2.1.1, tapable@^2.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
|
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
|
||||||
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
|
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
|
||||||
|
|
||||||
tar-js@^0.3.0:
|
|
||||||
version "0.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/tar-js/-/tar-js-0.3.0.tgz#6949aabfb0ba18bb1562ae51a439fd0f30183a17"
|
|
||||||
integrity sha512-9uqP2hJUZNKRkwPDe5nXxXdzo6w+BFBPq9x/tyi5/U/DneuSesO/HMb0y5TeWpfcv49YDJTs7SrrZeeu8ZHWDA==
|
|
||||||
|
|
||||||
tar-stream@^2.0.1:
|
tar-stream@^2.0.1:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
|
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
|
||||||
|
|||||||
Reference in New Issue
Block a user