useImages.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import { useState, useCallback, useEffect } from 'react';
  2. import { useBaaS, useAuth } from '@picobaas/client/react';
  3. import type { ImageFile, UploadOptions } from '../types';
  4. import { PUBLIC_BUCKET, USER_BUCKET, generateUUID } from '../config';
  5. import { getExpiryFromMetadata } from '../utils/expiry';
  6. import { getSessionToken, trackUpload, untrackUpload } from '../utils/session';
  7. import type { FileMetadata } from '@picobaas/client';
  8. export function useImages(bucket: string = PUBLIC_BUCKET) {
  9. const { client } = useBaaS();
  10. const { userId, isAuthenticated } = useAuth();
  11. const [images, setImages] = useState<ImageFile[]>([]);
  12. const [isLoading, setIsLoading] = useState(false);
  13. const [error, setError] = useState<string | null>(null);
  14. const fetchImages = useCallback(async () => {
  15. setIsLoading(true);
  16. setError(null);
  17. try {
  18. const storage = client.storage.from(bucket);
  19. const prefix = bucket === USER_BUCKET && userId ? `${userId}/` : '';
  20. // Fetch from storage
  21. const { data, error: listError } = await storage.list(prefix, {
  22. limit: 100,
  23. });
  24. if (listError) throw listError;
  25. // Also fetch from image API to get shortCodes
  26. const sessionToken = getSessionToken();
  27. const headers: Record<string, string> = {
  28. 'X-Session-Token': sessionToken,
  29. };
  30. const accessToken = client.accessToken;
  31. if (accessToken) {
  32. headers['Authorization'] = `Bearer ${accessToken}`;
  33. }
  34. let imageMetadata: Record<string, { shortCode: string; shortUrl: string; downloadAllowed: boolean }> = {};
  35. try {
  36. const apiResponse = await fetch('/api/images', { headers });
  37. if (apiResponse.ok) {
  38. const apiData = await apiResponse.json();
  39. // Create a lookup by path
  40. for (const img of apiData.items || []) {
  41. imageMetadata[img.path] = {
  42. shortCode: img.short_code,
  43. shortUrl: img.short_url,
  44. downloadAllowed: img.download_allowed,
  45. };
  46. }
  47. }
  48. } catch (e) {
  49. // Non-fatal, continue without shortCodes
  50. console.warn('Failed to fetch image metadata:', e);
  51. }
  52. const imageFiles: ImageFile[] = (data || []).map((file: FileMetadata) => {
  53. const meta = imageMetadata[file.path];
  54. return {
  55. id: file.id,
  56. path: file.path,
  57. name: file.filename || file.path.split('/').pop() || 'Unknown',
  58. size: file.size,
  59. mimeType: file.mime_type,
  60. url: storage.getPublicUrl(file.path).data.publicUrl,
  61. createdAt: new Date(file.created_at * 1000),
  62. expiresAt: getExpiryFromMetadata(file.custom_metadata),
  63. isPublic: bucket === PUBLIC_BUCKET,
  64. ownerId: file.owner_id,
  65. shortCode: meta?.shortCode,
  66. shortUrl: meta?.shortUrl,
  67. downloadAllowed: meta?.downloadAllowed,
  68. };
  69. });
  70. // Sort by created date, newest first
  71. imageFiles.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
  72. setImages(imageFiles);
  73. } catch (err) {
  74. setError(err instanceof Error ? err.message : 'Failed to load images');
  75. } finally {
  76. setIsLoading(false);
  77. }
  78. }, [client, bucket, userId]);
  79. const uploadImage = useCallback(
  80. async (file: File, options: UploadOptions): Promise<ImageFile | null> => {
  81. try {
  82. const storage = client.storage.from(bucket);
  83. // Generate unique filename
  84. const ext = file.name.split('.').pop() || 'jpg';
  85. const uniqueId = generateUUID();
  86. const prefix = bucket === USER_BUCKET && userId ? `${userId}/` : '';
  87. const path = `${prefix}${uniqueId}.${ext}`;
  88. // Calculate expiry
  89. const expiresAt =
  90. options.expirySeconds > 0
  91. ? new Date(Date.now() + options.expirySeconds * 1000).toISOString()
  92. : undefined;
  93. // Upload to storage
  94. const { data, error: uploadError } = await storage.upload(path, file, {
  95. contentType: file.type,
  96. metadata: expiresAt ? { expires_at: expiresAt } : undefined,
  97. });
  98. if (uploadError) throw uploadError;
  99. // Register with image API for short URL and tracking
  100. const sessionToken = getSessionToken();
  101. const headers: Record<string, string> = {
  102. 'Content-Type': 'application/json',
  103. 'X-Session-Token': sessionToken,
  104. };
  105. // Include auth token if logged in
  106. const accessToken = client.accessToken;
  107. if (accessToken) {
  108. headers['Authorization'] = `Bearer ${accessToken}`;
  109. }
  110. const apiResponse = await fetch('/api/images', {
  111. method: 'POST',
  112. headers,
  113. body: JSON.stringify({
  114. bucket,
  115. path,
  116. storage_object_id: data?.id,
  117. download_allowed: options.downloadAllowed,
  118. expires_at: expiresAt,
  119. }),
  120. });
  121. let shortCode: string | undefined;
  122. let shortUrl: string | undefined;
  123. if (apiResponse.ok) {
  124. const imageData = await apiResponse.json();
  125. shortCode = imageData.short_code;
  126. shortUrl = `/i/${shortCode}`;
  127. // Track anonymous uploads for deletion capability
  128. if (!isAuthenticated && shortCode) {
  129. trackUpload(shortCode);
  130. }
  131. } else {
  132. console.warn('Failed to register image with API:', await apiResponse.text());
  133. }
  134. const newImage: ImageFile = {
  135. id: data?.id || uniqueId,
  136. path: path,
  137. name: file.name,
  138. size: file.size,
  139. mimeType: file.type,
  140. url: storage.getPublicUrl(path).data.publicUrl,
  141. createdAt: new Date(),
  142. expiresAt: expiresAt ? new Date(expiresAt) : undefined,
  143. isPublic: bucket === PUBLIC_BUCKET,
  144. ownerId: userId || undefined,
  145. shortCode,
  146. shortUrl,
  147. downloadAllowed: options.downloadAllowed,
  148. };
  149. setImages((prev) => [newImage, ...prev]);
  150. return newImage;
  151. } catch (err) {
  152. throw new Error(
  153. err instanceof Error ? err.message : 'Upload failed'
  154. );
  155. }
  156. },
  157. [client, bucket, userId, isAuthenticated]
  158. );
  159. const deleteImage = useCallback(
  160. async (image: ImageFile): Promise<void> => {
  161. try {
  162. // If we have a short code, delete via the image API (handles session auth)
  163. if (image.shortCode) {
  164. const sessionToken = getSessionToken();
  165. const headers: Record<string, string> = {
  166. 'X-Session-Token': sessionToken,
  167. };
  168. const accessToken = client.accessToken;
  169. if (accessToken) {
  170. headers['Authorization'] = `Bearer ${accessToken}`;
  171. }
  172. const apiResponse = await fetch(`/api/images/${image.shortCode}`, {
  173. method: 'DELETE',
  174. headers,
  175. });
  176. if (!apiResponse.ok) {
  177. const errorData = await apiResponse.json().catch(() => ({}));
  178. throw new Error(errorData.error || 'Delete failed');
  179. }
  180. // Remove from session tracking
  181. untrackUpload(image.shortCode);
  182. } else {
  183. // Fallback to direct storage deletion
  184. const storage = client.storage.from(bucket);
  185. const { error: deleteError } = await storage.remove([image.path]);
  186. if (deleteError) throw deleteError;
  187. }
  188. setImages((prev) => prev.filter((img) => img.id !== image.id));
  189. } catch (err) {
  190. throw new Error(
  191. err instanceof Error ? err.message : 'Delete failed'
  192. );
  193. }
  194. },
  195. [client, bucket]
  196. );
  197. // Auto-fetch when authenticated for user bucket
  198. useEffect(() => {
  199. if (bucket === USER_BUCKET && isAuthenticated) {
  200. fetchImages();
  201. } else if (bucket === PUBLIC_BUCKET) {
  202. fetchImages();
  203. }
  204. }, [bucket, isAuthenticated, fetchImages]);
  205. return {
  206. images,
  207. isLoading,
  208. error,
  209. fetchImages,
  210. uploadImage,
  211. deleteImage,
  212. };
  213. }