Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 164 additions & 14 deletions src/pages/home/uploads/direct.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import { Upload, SetUpload } from "./types"
import { r, pathDir } from "~/utils"
import { Resp } from "~/types"
import { r, pathBase, pathDir } from "~/utils"

type DirectUploadCompletionInfo = {
url?: string
method?: string
headers?: Record<string, string>
body?: unknown
}

type PdsDirectUploadInfo = {
upload_url: string
headers?: Record<string, string>
method?: string
complete?: DirectUploadCompletionInfo
}

// Create a speed calculator using closure
function createSpeedCalculator(throttleMs = 500) {
Expand Down Expand Up @@ -80,6 +95,71 @@ export const HttpDirectUpload: Upload = async (
}
}

export const PdsDirectUpload: Upload = async (
uploadPath: string,
file: File,
setUpload: SetUpload,
_asTask: boolean,
overwrite: boolean,
_rapid: boolean,
) => {
const path = pathDir(uploadPath)

const resp = (await r.post(
"/fs/get_direct_upload_info",
{
path,
file_name: file.name,
file_size: file.size,
tool: "PdsDirect",
},
{
headers: {
"File-Path": encodeURIComponent(uploadPath),
Overwrite: overwrite,
},
},
)) as Resp<PdsDirectUploadInfo | null>

if (resp.code !== 200) {
throw new Error(resp.message)
}

const uploadInfo = resp.data

if (!uploadInfo?.upload_url) {
throw new Error("PDS Direct Upload not supported")
}

await uploadSingle(
file,
uploadInfo.upload_url,
uploadInfo.method || "PUT",
uploadInfo.headers,
setUpload,
)

await completeDirectUpload(uploadInfo.complete, uploadPath, setUpload)
return undefined
}

function getHeaderEntries(headers?: Record<string, string>) {
return Object.entries(headers ?? {}).filter(([, value]) => value !== "")
}

function shouldSuppressContentType(headers?: Record<string, string>) {
return Object.entries(headers ?? {}).some(
([key, value]) => key.toLowerCase() === "content-type" && value === "",
)
}

function getRequestBody(blob: Blob, suppressContentType: boolean): Blob {
if (!suppressContentType || blob.type === "") {
return blob
}
return blob.slice(0, blob.size, "")
}

async function uploadSingle(
file: File,
uploadURL: string,
Expand All @@ -89,6 +169,7 @@ async function uploadSingle(
): Promise<undefined> {
const xhr = new XMLHttpRequest()
const calcSpeed = createSpeedCalculator()
const suppressContentType = shouldSuppressContentType(headers)

return new Promise((resolve, reject) => {
xhr.upload.addEventListener("progress", (e) => {
Expand All @@ -113,14 +194,11 @@ async function uploadSingle(

xhr.open(method, uploadURL)

// Set custom headers if provided
if (headers) {
Object.entries(headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value)
})
}
getHeaderEntries(headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value)
})

xhr.send(file)
xhr.send(getRequestBody(file, suppressContentType))
})
}

Expand Down Expand Up @@ -176,16 +254,88 @@ async function uploadChunked(
`bytes ${start}-${end - 1}/${file.size}`,
)

// Set custom headers if provided
if (headers) {
Object.entries(headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value)
})
}
getHeaderEntries(headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value)
})

xhr.send(chunk)
})
}

return undefined
}

async function completeDirectUpload(
complete?: DirectUploadCompletionInfo,
uploadPath?: string,
setUpload?: SetUpload,
): Promise<void> {
if (!complete) {
return
}
if (!complete.url) {
throw new Error("Direct upload completion URL is missing")
}

setUpload?.("status", "backending")

const headers = new Headers()
getHeaderEntries(complete.headers).forEach(([key, value]) => {
headers.set(key, value)
})
if (uploadPath && !headers.has("File-Path")) {
headers.set("File-Path", encodeURIComponent(uploadPath))
}
if (!headers.has("Authorization")) {
const token = localStorage.getItem("token")
if (token) {
headers.set("Authorization", token)
}
}

let body: BodyInit | undefined
const completeBody = normalizeCompleteBody(complete.body, uploadPath)
if (completeBody !== undefined && completeBody !== null) {
if (typeof completeBody === "string") {
body = completeBody
} else {
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/json")
}
body = JSON.stringify(completeBody)
}
}

const resp = await fetch(complete.url, {
method: complete.method || "POST",
headers,
body,
})
const text = await resp.text()
let data: Resp<unknown> | undefined
try {
data = text ? (JSON.parse(text) as Resp<unknown>) : undefined
} catch {
data = undefined
}
if (!resp.ok) {
throw new Error(data?.message || `Complete upload failed: ${resp.status}`)
}
if (data && data.code !== 200) {
throw new Error(data.message || "Complete upload failed")
}
}

function normalizeCompleteBody(body: unknown, uploadPath?: string): unknown {
if (!uploadPath || typeof body !== "object" || body === null) {
return body
}
if (Array.isArray(body)) {
return body
}
return {
...(body as Record<string, unknown>),
path: pathDir(uploadPath),
file_name: pathBase(uploadPath),
}
}
9 changes: 8 additions & 1 deletion src/pages/home/uploads/uploads.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { objStore } from "~/store"
import { FormUpload } from "./form"
import { StreamUpload } from "./stream"
import { HttpDirectUpload } from "./direct"
import { HttpDirectUpload, PdsDirectUpload } from "./direct"
import { Upload } from "./types"

type Uploader = {
Expand All @@ -19,6 +19,13 @@ const AllUploads: Uploader[] = [
return objStore.direct_upload_tools?.includes("HttpDirect") || false
},
},
{
name: "PDS Direct",
upload: PdsDirectUpload,
available: () => {
return objStore.direct_upload_tools?.includes("PdsDirect") || false
},
},
{
name: "Stream",
upload: StreamUpload,
Expand Down