Files

155 lines
3.8 KiB
TypeScript

interface Env {
FILES_BUCKET: R2Bucket
}
interface FileItem {
name: string
key: string
size: number
lastModified: string
type: "file" | "folder"
children?: FileItem[]
}
interface FileNode {
[key: string]: FileNode | R2Object
}
// Build tree structure from flat file list
function buildTree(objects: R2Object[]): FileItem[] {
const root: FileNode = {}
for (const obj of objects) {
const parts = obj.key.split("/")
let current = root
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
if (!part) continue
if (i === parts.length - 1) {
// This is a file
current[part] = obj
} else {
// This is a folder
if (
!current[part] ||
(typeof current[part] === "object" && "key" in current[part])
) {
current[part] = {}
}
current = current[part] as FileNode
}
}
}
return nodeToFileItems(root, "")
}
function nodeToFileItems(node: FileNode, basePath: string): FileItem[] {
const items: FileItem[] = []
for (const [name, value] of Object.entries(node)) {
if (
value &&
typeof value === "object" &&
"key" in value &&
"size" in value
) {
// This is an R2Object (file)
const obj = value as R2Object
items.push({
name,
key: obj.key,
size: obj.size,
lastModified: obj.uploaded.toISOString(),
type: "file",
})
} else {
// This is a folder
const folderPath = basePath ? `${basePath}/${name}` : name
items.push({
name,
key: folderPath,
size: 0,
lastModified: "",
type: "folder",
children: nodeToFileItems(value as FileNode, folderPath),
})
}
}
// Sort: folders first, then files, alphabetically
return items.sort((a, b) => {
if (a.type !== b.type) {
return a.type === "folder" ? -1 : 1
}
return a.name.localeCompare(b.name)
})
}
export const onRequestGet: PagesFunction<Env> = async (context) => {
const pathParts = context.params.path as string[] | undefined
const path = pathParts?.join("/") || ""
// If no path or path is "list", return file listing
if (!path || path === "list") {
try {
const listed = await context.env.FILES_BUCKET.list()
const tree = buildTree(listed.objects)
return new Response(JSON.stringify({ files: tree }), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
})
} catch (error) {
console.error("Error listing files:", error)
return new Response(JSON.stringify({ error: "Failed to list files" }), {
status: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
})
}
}
// Otherwise, download the file
try {
const object = await context.env.FILES_BUCKET.get(path)
if (!object) {
return new Response("Not Found", { status: 404 })
}
const headers = new Headers()
headers.set(
"Content-Type",
object.httpMetadata?.contentType || "application/octet-stream",
)
headers.set("Content-Length", object.size.toString())
headers.set(
"Content-Disposition",
`attachment; filename="${path.split("/").pop()}"`,
)
headers.set("Access-Control-Allow-Origin", "*")
return new Response(object.body, { headers })
} catch (error) {
console.error("Error downloading file:", error)
return new Response("Internal Server Error", { status: 500 })
}
}
export const onRequestOptions: PagesFunction<Env> = async () => {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
})
}