useImages.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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 using SDK's request method
  26. const sessionToken = getSessionToken();
  27. let imageMetadata: Record<string, { shortCode: string; shortUrl: string; downloadAllowed: boolean }> = {};
  28. try {
  29. const apiResponse = await client.request<{ items: Array<{ path: string; short_code: string; short_url: string; download_allowed: boolean }> }>(
  30. '/images',
  31. {
  32. headers: { 'X-Session-Token': sessionToken },
  33. }
  34. );
  35. if (!apiResponse.error && apiResponse.data) {
  36. // Create a lookup by path
  37. for (const img of apiResponse.data.items || []) {
  38. imageMetadata[img.path] = {
  39. shortCode: img.short_code,
  40. shortUrl: img.short_url,
  41. downloadAllowed: img.download_allowed,
  42. };
  43. }
  44. }
  45. } catch (e) {
  46. // Non-fatal, continue without shortCodes
  47. console.warn('Failed to fetch image metadata:', e);
  48. }
  49. const imageFiles: ImageFile[] = (data || []).map((file: FileMetadata) => {
  50. const meta = imageMetadata[file.path];
  51. return {
  52. id: file.id,
  53. path: file.path,
  54. name: file.filename || file.path.split('/').pop() || 'Unknown',
  55. size: file.size,
  56. mimeType: file.mime_type,
  57. url: storage.getPublicUrl(file.path).data.publicUrl,
  58. createdAt: new Date(file.created_at * 1000),
  59. expiresAt: getExpiryFromMetadata(file.custom_metadata),
  60. isPublic: bucket === PUBLIC_BUCKET,
  61. ownerId: file.owner_id,
  62. shortCode: meta?.shortCode,
  63. shortUrl: meta?.shortUrl,
  64. downloadAllowed: meta?.downloadAllowed,
  65. };
  66. });
  67. // Sort by created date, newest first
  68. imageFiles.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
  69. setImages(imageFiles);
  70. } catch (err) {
  71. setError(err instanceof Error ? err.message : 'Failed to load images');
  72. } finally {
  73. setIsLoading(false);
  74. }
  75. }, [client, bucket, userId]);
  76. const uploadImage = useCallback(
  77. async (file: File, options: UploadOptions): Promise<ImageFile | null> => {
  78. try {
  79. const storage = client.storage.from(bucket);
  80. // Generate unique filename
  81. const ext = file.name.split('.').pop() || 'jpg';
  82. const uniqueId = generateUUID();
  83. const prefix = bucket === USER_BUCKET && userId ? `${userId}/` : '';
  84. const path = `${prefix}${uniqueId}.${ext}`;
  85. // Calculate expiry
  86. const expiresAt =
  87. options.expirySeconds > 0
  88. ? new Date(Date.now() + options.expirySeconds * 1000).toISOString()
  89. : undefined;
  90. // Upload to storage
  91. const { data, error: uploadError } = await storage.upload(path, file, {
  92. contentType: file.type,
  93. metadata: expiresAt ? { expires_at: expiresAt } : undefined,
  94. });
  95. if (uploadError) throw uploadError;
  96. let shortCode: string | undefined;
  97. let shortUrl: string | undefined;
  98. // Only register with image API for anonymous users (auto short link generation)
  99. // Logged-in users manage share links manually via the dashboard
  100. if (!isAuthenticated) {
  101. const sessionToken = getSessionToken();
  102. const apiResponse = await client.request<{ short_code: string }>(
  103. '/images',
  104. {
  105. method: 'POST',
  106. headers: { 'X-Session-Token': sessionToken },
  107. body: JSON.stringify({
  108. bucket,
  109. path,
  110. storage_object_id: data?.id,
  111. download_allowed: options.downloadAllowed,
  112. expires_at: expiresAt,
  113. }),
  114. }
  115. );
  116. if (!apiResponse.error && apiResponse.data) {
  117. shortCode = apiResponse.data.short_code;
  118. shortUrl = `/i/${shortCode}`;
  119. // Track anonymous uploads for deletion capability
  120. trackUpload(shortCode);
  121. } else {
  122. console.warn('Failed to register image with API:', apiResponse.error);
  123. }
  124. }
  125. const newImage: ImageFile = {
  126. id: data?.id || uniqueId,
  127. path: path,
  128. name: file.name,
  129. size: file.size,
  130. mimeType: file.type,
  131. url: storage.getPublicUrl(path).data.publicUrl,
  132. createdAt: new Date(),
  133. expiresAt: expiresAt ? new Date(expiresAt) : undefined,
  134. isPublic: bucket === PUBLIC_BUCKET,
  135. ownerId: userId || undefined,
  136. shortCode,
  137. shortUrl,
  138. downloadAllowed: options.downloadAllowed,
  139. };
  140. setImages((prev) => [newImage, ...prev]);
  141. return newImage;
  142. } catch (err) {
  143. throw new Error(
  144. err instanceof Error ? err.message : 'Upload failed'
  145. );
  146. }
  147. },
  148. [client, bucket, userId, isAuthenticated]
  149. );
  150. const deleteImage = useCallback(
  151. async (image: ImageFile): Promise<void> => {
  152. try {
  153. // If we have a short code, delete via the image API using SDK
  154. if (image.shortCode) {
  155. const sessionToken = getSessionToken();
  156. const apiResponse = await client.request(
  157. `/images/${image.shortCode}`,
  158. {
  159. method: 'DELETE',
  160. headers: { 'X-Session-Token': sessionToken },
  161. }
  162. );
  163. if (apiResponse.error) {
  164. throw new Error(apiResponse.error.message || 'Delete failed');
  165. }
  166. // Remove from session tracking
  167. untrackUpload(image.shortCode);
  168. } else {
  169. // Fallback to direct storage deletion
  170. const storage = client.storage.from(bucket);
  171. const { error: deleteError } = await storage.remove([image.path]);
  172. if (deleteError) throw deleteError;
  173. }
  174. setImages((prev) => prev.filter((img) => img.id !== image.id));
  175. } catch (err) {
  176. throw new Error(
  177. err instanceof Error ? err.message : 'Delete failed'
  178. );
  179. }
  180. },
  181. [client, bucket]
  182. );
  183. // Auto-fetch when authenticated for user bucket
  184. useEffect(() => {
  185. if (bucket === USER_BUCKET && isAuthenticated) {
  186. fetchImages();
  187. } else if (bucket === PUBLIC_BUCKET) {
  188. fetchImages();
  189. }
  190. }, [bucket, isAuthenticated, fetchImages]);
  191. return {
  192. images,
  193. isLoading,
  194. error,
  195. fetchImages,
  196. uploadImage,
  197. deleteImage,
  198. };
  199. }