-
-
Notifications
You must be signed in to change notification settings - Fork 264
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: Add support for uploading images and automatically inferring…
… their tags (#2) * feature: Experimental support for asset uploads * feature(web): Add new bookmark type asset * feature: Add support for automatically tagging images * fix: Add support for image assets in preview page * use next Image for fetching the images * Fix auth and error codes in the route handlers * Add support for image uploads on mobile * Fix typing of upload requests * Remove the ugly dragging box * Bump mobile version to 1.3 * Change the editor card placeholder to mention uploading images * Fix a typo * Change ios icon for photo library * Silence typescript error
- Loading branch information
1 parent
5495209
commit 785a5b5
Showing
31 changed files
with
2,736 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
pnpm typecheck | ||
pnpm format | ||
pnpm lint |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { useMutation } from "@tanstack/react-query"; | ||
|
||
import type { Settings } from "./settings"; | ||
import { api } from "./trpc"; | ||
import type { ZBookmark } from "@hoarder/trpc/types/bookmarks"; | ||
import { zUploadResponseSchema, zUploadErrorSchema } from "@hoarder/trpc/types/uploads"; | ||
|
||
export function useUploadAsset( | ||
settings: Settings, | ||
options: { onSuccess?: (bookmark: ZBookmark) => void; onError?: (e: string) => void }, | ||
) { | ||
const invalidateAllBookmarks = | ||
api.useUtils().bookmarks.getBookmarks.invalidate; | ||
|
||
const { | ||
mutate: createBookmark, | ||
isPending: isCreatingBookmark, | ||
} = api.bookmarks.createBookmark.useMutation({ | ||
onSuccess: (d) => { | ||
invalidateAllBookmarks(); | ||
if (options.onSuccess) { | ||
options.onSuccess(d); | ||
} | ||
}, | ||
onError: (e) => { | ||
if (options.onError) { | ||
options.onError(e.message); | ||
} | ||
}, | ||
}); | ||
|
||
const { | ||
mutate: uploadAsset, | ||
isPending: isUploading, | ||
} = useMutation({ | ||
mutationFn: async (file: { type: string; name: string; uri: string }) => { | ||
const formData = new FormData(); | ||
// @ts-expect-error This is a valid api in react native | ||
formData.append("image", { | ||
uri: file.uri, | ||
name: file.name, | ||
type: file.type, | ||
}); | ||
const resp = await fetch(`${settings.address}/api/assets`, { | ||
method: "POST", | ||
body: formData, | ||
headers: { | ||
Authorization: `Bearer ${settings.apiKey}`, | ||
}, | ||
}); | ||
if (!resp.ok) { | ||
throw new Error(await resp.text()); | ||
} | ||
return zUploadResponseSchema.parse(await resp.json()); | ||
}, | ||
onSuccess: (resp) => { | ||
const assetId = resp.assetId; | ||
createBookmark({ type: "asset", assetId, assetType: "image" }); | ||
}, | ||
onError: (e) => { | ||
if (options.onError) { | ||
const err = zUploadErrorSchema.parse(JSON.parse(e.message)); | ||
options.onError(err.error); | ||
} | ||
}, | ||
}); | ||
|
||
return { | ||
uploadAsset, | ||
isPending: isUploading || isCreatingBookmark, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { createContextFromRequest } from "@/server/api/client"; | ||
import { and, eq } from "drizzle-orm"; | ||
|
||
import { db } from "@hoarder/db"; | ||
import { assets } from "@hoarder/db/schema"; | ||
|
||
export const dynamic = "force-dynamic"; | ||
export async function GET( | ||
request: Request, | ||
{ params }: { params: { assetId: string } }, | ||
) { | ||
const ctx = await createContextFromRequest(request); | ||
if (!ctx.user) { | ||
return Response.json({ error: "Unauthorized" }, { status: 401 }); | ||
} | ||
const asset = await db.query.assets.findFirst({ | ||
where: and(eq(assets.id, params.assetId), eq(assets.userId, ctx.user.id)), | ||
}); | ||
|
||
if (!asset) { | ||
return Response.json({ error: "Asset not found" }, { status: 404 }); | ||
} | ||
return new Response(asset.blob as string, { | ||
status: 200, | ||
headers: { | ||
"Content-type": asset.contentType, | ||
}, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { createContextFromRequest } from "@/server/api/client"; | ||
|
||
import type { ZUploadResponse } from "@hoarder/trpc/types/uploads"; | ||
import { db } from "@hoarder/db"; | ||
import { assets } from "@hoarder/db/schema"; | ||
|
||
const SUPPORTED_ASSET_TYPES = new Set(["image/jpeg", "image/png"]); | ||
|
||
const MAX_UPLOAD_SIZE_BYTES = 4 * 1024 * 1024; | ||
|
||
export const dynamic = "force-dynamic"; | ||
export async function POST(request: Request) { | ||
const ctx = await createContextFromRequest(request); | ||
if (!ctx.user) { | ||
return Response.json({ error: "Unauthorized" }, { status: 401 }); | ||
} | ||
const formData = await request.formData(); | ||
const data = formData.get("image"); | ||
let buffer; | ||
let contentType; | ||
if (data instanceof File) { | ||
contentType = data.type; | ||
if (!SUPPORTED_ASSET_TYPES.has(contentType)) { | ||
return Response.json( | ||
{ error: "Unsupported asset type" }, | ||
{ status: 400 }, | ||
); | ||
} | ||
if (data.size > MAX_UPLOAD_SIZE_BYTES) { | ||
return Response.json({ error: "Asset is too big" }, { status: 413 }); | ||
} | ||
buffer = Buffer.from(await data.arrayBuffer()); | ||
} else { | ||
return Response.json({ error: "Bad request" }, { status: 400 }); | ||
} | ||
|
||
const [dbRes] = await db | ||
.insert(assets) | ||
.values({ | ||
encoding: "binary", | ||
contentType: contentType, | ||
blob: buffer, | ||
userId: ctx.user.id, | ||
}) | ||
.returning(); | ||
|
||
return Response.json({ | ||
assetId: dbRes.id, | ||
contentType: dbRes.contentType, | ||
size: buffer.byteLength, | ||
} satisfies ZUploadResponse); | ||
} |
Oops, something went wrong.