diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index f571a94..938aaaa 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -19,6 +19,7 @@ func main() { r.GET("/api/v1/health", handlers.Health) r.GET("/api/v1/files", handlers.ListFiles) + r.GET("/api/v1/file/:id", handlers.ListFile) r.POST("/api/v1/files/upload", handlers.UploadFile) r.GET("/api/v1/files/:id", handlers.GetFile) r.DELETE("/api/v1/files/:id", handlers.DeleteFile) diff --git a/backend/internal/handlers/files.go b/backend/internal/handlers/files.go index 37d8310..d07377a 100644 --- a/backend/internal/handlers/files.go +++ b/backend/internal/handlers/files.go @@ -70,6 +70,71 @@ func ListFiles(c *gin.Context) { }) } +func ListFile(c *gin.Context) { + + id := c.Query("id") + + dirs, err := os.ReadDir("./uploads/") + + if err != nil { + + c.JSON(http.StatusInternalServerError, gin.H{ + "data": []interface{}{}, + "meta": gin.H{ + "page": 1, + "total": 0, + }, + }) + + return + } + + type Finfo struct { + Url string `json:"url"` + Name string `json:"name"` + Size int64 `json:"size"` + Tags []string `json:"tags"` + } + + file := Finfo{} + + for _, dir := range dirs { + + if dir.IsDir() { + continue + } + + i, e := dir.Info() + + if e != nil { + continue + } + + if id != i.Name() { + continue + } + + ps := Finfo{ + Url: service.GenerateFileDownloadUrl(i.Name()), + Name: i.Name(), + Size: i.Size(), + Tags: []string{}, + } + + file = ps + + break + + } + + c.JSON(http.StatusOK, gin.H{ + "data": file, + "meta": gin.H{ + "total": 1, + }, + }) +} + func GetFile(c *gin.Context) { exp, _ := strconv.ParseInt(c.Query("expires"), 10, 64) @@ -78,7 +143,7 @@ func GetFile(c *gin.Context) { if r != true { - c.JSON(http.StatusNotFound, gin.H{ + c.JSON(http.StatusForbidden, gin.H{ "error": gin.H{ "code": "FILE_NOT_FOUND", "message": "File not found", diff --git a/mobile/components/FileCard.tsx b/mobile/components/FileCard.tsx index c9c21f0..91740f9 100644 --- a/mobile/components/FileCard.tsx +++ b/mobile/components/FileCard.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { View, Text, Image, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native'; import { FileItem } from '../types'; import { TagChip } from './TagChip'; +import { useFileImage } from '../hooks/useFileImage'; interface FileCardProps { file: FileItem; @@ -9,16 +10,25 @@ interface FileCardProps { } export function FileCard({ file, onPress }: FileCardProps) { + const { localUri, loading } = useFileImage(file.url); +console.log(localUri) const formatSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; - console.log(file) - return ( onPress?.(file)}> + {file.url && ( + + {loading && } + {localUri && ( + + )} + + )} + {file.name} @@ -53,6 +63,25 @@ const styles = StyleSheet.create({ shadowRadius: 2, elevation: 2, }, + imageContainer: { + width: '100%', + height: 200, + borderRadius: 8, + overflow: 'hidden', + marginBottom: 12, + backgroundColor: '#f0f0f0', + }, + image: { + width: '100%', + height: '100%', + }, + imageLoader: { + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -10, + marginLeft: -10, + }, header: { flexDirection: 'row', justifyContent: 'space-between', diff --git a/mobile/hooks/useFileImage.ts b/mobile/hooks/useFileImage.ts new file mode 100644 index 0000000..a21b38f --- /dev/null +++ b/mobile/hooks/useFileImage.ts @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react'; +import { downloadAsync, documentDirectory, makeDirectoryAsync, getInfoAsync } from 'expo-file-system/legacy'; + +const CACHE_DIR = `${documentDirectory}file-images/`; + +function getCacheKey(url: string): string { + let hash = 0; + for (let i = 0; i < url.length; i++) { + hash = ((hash << 5) - hash + url.charCodeAt(i)) | 0; + } + return Math.abs(hash).toString(36); +} + +function getExtension(url: string): string { + const pathname = new URL(url).pathname; + const dot = pathname.lastIndexOf('.'); + return dot >= 0 ? pathname.slice(dot) : '.jpg'; +} + +export function useFileImage(url: string | undefined) { + const [localUri, setLocalUri] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (!url) return; + + const resolvedUrl = url; + let cancelled = false; + + async function load() { + const fileName = `${getCacheKey(resolvedUrl)}${getExtension(resolvedUrl)}`; + const fileUri = `${CACHE_DIR}${fileName}`; + + const info = await getInfoAsync(fileUri); + if (info.exists) { + if (!cancelled) setLocalUri(info.uri); + return; + } + + setLoading(true); + try { + await makeDirectoryAsync(CACHE_DIR, { intermediates: true }); + const result = await downloadAsync(resolvedUrl, fileUri); + if (!cancelled) { + setLocalUri(result.uri); + } + } catch { + if (!cancelled) { + setLocalUri(null); + } + } finally { + if (!cancelled) setLoading(false); + } + } + + load(); + + return () => { + cancelled = true; + }; + }, [url]); + + return { localUri, loading }; +} diff --git a/mobile/types/index.ts b/mobile/types/index.ts index 3144d00..dfbfda4 100644 --- a/mobile/types/index.ts +++ b/mobile/types/index.ts @@ -7,6 +7,7 @@ export interface FileItem { updatedAt: string; ocrText?: string; tags: Tag[]; + url?: string; } export interface Tag {