-
Notifications
You must be signed in to change notification settings - Fork 0
host and upload images #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,22 @@ | ||
|
|
||
| import {useLocation} from "react-router"; | ||
|
|
||
| const VisualizerId = () => { | ||
| return ( | ||
| <div> | ||
| VisualizerId | ||
| </div> | ||
| ) | ||
| } | ||
| const location = useLocation(); | ||
| const { initialImage, name } = location.state || {}; | ||
|
|
||
| export default VisualizerId | ||
| return ( | ||
| <section> | ||
| <h1> {name || 'Untitled Project'}</h1> | ||
|
|
||
| <div className="visualizer"> | ||
| {initialImage && ( | ||
| <div className="image-container"> | ||
| <h2>Source Image</h2> | ||
| <img src={initialImage} alt="source" /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </section> | ||
| ) | ||
| } | ||
| export default VisualizerId |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,13 +1,65 @@ | ||||||||||
| import puter from "@heyputer/puter.js"; | ||||||||||
| import {getOrCreateHostingConfig, uploadImageToHosting} from "./puter.hosting"; | ||||||||||
| import {isHostedUrl} from "./utils"; | ||||||||||
|
|
||||||||||
| export const signIn = async () => await puter.auth.signIn(); | ||||||||||
|
|
||||||||||
| export const signOut = () => puter.auth.signOut(); | ||||||||||
|
|
||||||||||
| export const getCurrentUser = async () =>{ | ||||||||||
| try{ | ||||||||||
| export const getCurrentUser = async () => { | ||||||||||
| try { | ||||||||||
| return await puter.auth.getUser(); | ||||||||||
| }catch{ | ||||||||||
| return null | ||||||||||
| } catch { | ||||||||||
| return null; | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| export const createProject = async ({ item }: CreateProjectParams): Promise<DesignItem | null | undefined> => { | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The The function destructures only 🔧 Option A: Use the parameter-export const createProject = async ({ item }: CreateProjectParams): Promise<DesignItem | null | undefined> => {
+export const createProject = async ({ item, visibility = 'private' }: CreateProjectParams): Promise<DesignItem | null> => {
const projectId = item.id;
+ // Use visibility when storing...📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| const projectId = item.id; | ||||||||||
|
|
||||||||||
| const hosting = await getOrCreateHostingConfig(); | ||||||||||
|
|
||||||||||
| const hostedSource = projectId ? | ||||||||||
| await uploadImageToHosting({ hosting, url: item.sourceImage, projectId, label: 'source', }) : null; | ||||||||||
|
|
||||||||||
| const hostedRender = projectId && item.renderedImage ? | ||||||||||
| await uploadImageToHosting({ hosting, url: item.renderedImage, projectId, label: 'rendered', }) : null; | ||||||||||
|
|
||||||||||
| const resolvedSource = hostedSource?.url || (isHostedUrl(item.sourceImage) | ||||||||||
| ? item.sourceImage | ||||||||||
| : '' | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| if(!resolvedSource) { | ||||||||||
| console.warn('Failed to host source image, skipping save.') | ||||||||||
| return null; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const resolvedRender = hostedRender?.url | ||||||||||
| ? hostedRender?.url | ||||||||||
| : item.renderedImage && isHostedUrl(item.renderedImage) | ||||||||||
| ? item.renderedImage | ||||||||||
| : undefined; | ||||||||||
|
|
||||||||||
| const { | ||||||||||
| sourcePath: _sourcePath, | ||||||||||
| renderedPath: _renderedPath, | ||||||||||
| publicPath: _publicPath, | ||||||||||
| ...rest | ||||||||||
| } = item; | ||||||||||
|
|
||||||||||
| const payload = { | ||||||||||
| ...rest, | ||||||||||
| sourceImage: resolvedSource, | ||||||||||
| renderedImage: resolvedRender, | ||||||||||
| } | ||||||||||
|
|
||||||||||
| try { | ||||||||||
| // Call the Puter worker to store project in kv | ||||||||||
|
|
||||||||||
| return payload; | ||||||||||
|
Comment on lines
+57
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Project persistence is not implemented. The Based on the pattern in 🔧 Suggested implementation try {
// Call the Puter worker to store project in kv
-
+ await puter.kv.set(`project:${projectId}`, payload);
return payload;
} catch (e) {🤖 Prompt for AI Agents |
||||||||||
| } catch (e) { | ||||||||||
| console.log('Failed to save project', e) | ||||||||||
| return null; | ||||||||||
| } | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import puter from "@heyputer/puter.js"; | ||
| import { | ||
| createHostingSlug, | ||
| fetchBlobFromUrl, getHostedUrl, | ||
| getImageExtension, | ||
| HOSTING_CONFIG_KEY, | ||
| imageUrlToPngBlob, | ||
| isHostedUrl | ||
| } from "./utils"; | ||
|
|
||
| export const getOrCreateHostingConfig = async (): Promise<HostingConfig | null> => { | ||
| const existing = (await puter.kv.get(HOSTING_CONFIG_KEY)) as HostingConfig | null; | ||
|
|
||
| if(existing?.subdomain) return { subdomain: existing.subdomain }; | ||
|
|
||
| const subdomain = createHostingSlug(); | ||
|
|
||
| try { | ||
| const created = await puter.hosting.create(subdomain, '.'); | ||
|
|
||
| const record = { subdomain: created.subdomain }; | ||
|
|
||
| await puter.kv.set(HOSTING_CONFIG_KEY,record); | ||
|
|
||
| return record; | ||
|
|
||
|
|
||
| } catch (e) { | ||
| console.warn(`Could not find subdomain: ${e}`); | ||
| return null; | ||
| } | ||
|
Comment on lines
+11
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Race condition in This function has a TOCTOU (time-of-check-time-of-use) vulnerability. Between checking
This can occur when a user rapidly uploads multiple images before the first 🔒 Suggested mitigation using module-level promise caching+let hostingConfigPromise: Promise<HostingConfig | null> | null = null;
+
-export const getOrCreateHostingConfig = async (): Promise<HostingConfig | null> => {
+export const getOrCreateHostingConfig = (): Promise<HostingConfig | null> => {
+ if (hostingConfigPromise) return hostingConfigPromise;
+
+ hostingConfigPromise = (async () => {
const existing = (await puter.kv.get(HOSTING_CONFIG_KEY)) as HostingConfig | null;
-
if(existing?.subdomain) return { subdomain: existing.subdomain };
const subdomain = createHostingSlug();
try {
const created = await puter.hosting.create(subdomain, '.');
-
const record = { subdomain: created.subdomain };
-
await puter.kv.set(HOSTING_CONFIG_KEY, record);
-
return record;
-
-
} catch (e) {
console.warn(`Could not find subdomain: ${e}`);
+ hostingConfigPromise = null; // Allow retry on failure
return null;
}
-}
+ })();
+
+ return hostingConfigPromise;
+};🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| export const uploadImageToHosting = async ({ hosting, url, projectId, label }: StoreHostedImageParams): Promise<HostedAsset | null> => { | ||
| if(!hosting || !url) return null; | ||
| if(isHostedUrl(url)) return { url }; | ||
|
|
||
| try { | ||
| const resolved = label === "rendered" | ||
| ? await imageUrlToPngBlob(url) | ||
| .then((blob) => blob ? { blob, contentType: 'image/png' }: null) | ||
| : await fetchBlobFromUrl(url); | ||
|
|
||
| if(!resolved) return null; | ||
|
|
||
| const contentType = resolved.contentType || resolved.blob.type || ''; | ||
| const ext = getImageExtension(contentType, url); | ||
| const dir = `projects/${projectId}`; | ||
| const filePath = `${dir}/${label}.${ext}`; | ||
|
|
||
| const uploadFile = new File([resolved.blob], `${label}.${ext}`, { | ||
| type: contentType, | ||
| }); | ||
|
|
||
| await puter.fs.mkdir(dir, { createMissingParents: true }); | ||
| await puter.fs.write(filePath, uploadFile); | ||
|
|
||
| const hostedUrl = getHostedUrl({ subdomain: hosting.subdomain }, filePath); | ||
|
|
||
| return hostedUrl ? { url: hostedUrl } : null; | ||
| } catch (e) { | ||
| console.warn(`Failed to store hosted image: ${e}`); | ||
| return null; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing
keyprop in list rendering.The
projects.map()renders items without akeyprop, which will cause React reconciliation issues and trigger a console warning.🔧 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents