diff --git a/backend/internal/handlers/files.go b/backend/internal/handlers/files.go index 32916f3..bde7b9e 100644 --- a/backend/internal/handlers/files.go +++ b/backend/internal/handlers/files.go @@ -1,14 +1,64 @@ package handlers import ( + "fmt" "net/http" + "os" + "path/filepath" "github.com/gin-gonic/gin" ) func ListFiles(c *gin.Context) { + + dirs, err := os.ReadDir("./uploads/") + + if err != nil { + + fmt.Println("What") + c.JSON(http.StatusInternalServerError, gin.H{ + "data": []interface{}{}, + "meta": gin.H{ + "page": 1, + "total": 0, + }, + }) + + return + } + + type Finfo struct { + Name string `json:"name"` + Size int64 `json:"size"` + Tags []string `json:"tags"` + } + + files := []Finfo{} + + for _, dir := range dirs { + + if dir.IsDir() { + continue + } + + i, e := dir.Info() + + if e != nil { + continue + } + + ps := Finfo{ + Name: i.Name(), + Size: i.Size(), + Tags: []string{}, + } + + files = append(files, ps) + + } + c.JSON(http.StatusOK, gin.H{ - "data": []interface{}{}, + "data": files, "meta": gin.H{ "page": 1, "total": 0, @@ -26,6 +76,24 @@ func GetFile(c *gin.Context) { } func UploadFile(c *gin.Context) { + + file, err := c.FormFile("file") + + if err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "error": gin.H{ + "code": "NOT_FOUND", + "message": "form file with file name missing", + }, + }) + } + + dst := filepath.Join("./uploads/", filepath.Base(file.Filename)) + + c.SaveUploadedFile(file, dst) + + c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename)) + c.JSON(http.StatusNotImplemented, gin.H{ "error": gin.H{ "code": "NOT_IMPLEMENTED", diff --git a/backend/internal/service/upload.go b/backend/internal/service/upload.go new file mode 100644 index 0000000..d01a6ae --- /dev/null +++ b/backend/internal/service/upload.go @@ -0,0 +1,10 @@ +package service + +import ( + "fmt" + "mime/multipart" +) + +func UploadFile(f *multipart.FileHeader) { + fmt.Println(f.Filename) +} diff --git a/mobile/api/client.ts b/mobile/api/client.ts index d4d842d..eeb5df9 100644 --- a/mobile/api/client.ts +++ b/mobile/api/client.ts @@ -43,24 +43,6 @@ class ApiClient { async delete(endpoint: string): Promise { return this.request(endpoint, { method: 'DELETE' }); } - - async uploadFile(endpoint: string, formData: FormData): Promise { - const url = `${this.baseUrl}${endpoint}`; - const response = await fetch(url, { - method: 'POST', - body: formData, - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - - if (!response.ok) { - const error: ApiError = await response.json(); - throw new Error(error.error?.message || 'Upload failed'); - } - - return response.json(); - } } export const apiClient = new ApiClient(API_BASE_URL); diff --git a/mobile/app.json b/mobile/app.json index a7f6f19..c7d420f 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -7,9 +7,14 @@ "icon": "./assets/icon.png", "userInterfaceStyle": "light", "ios": { - "supportsTablet": true + "supportsTablet": true, + "infoPlist": { + "NSCameraUsageDescription": "VaultDrop a besoin d'accéder à votre caméra pour scanner des documents.", + "NSPhotoLibraryUsageDescription": "VaultDrop a besoin d'accéder à votre galerie pour sélectionner des photos." + } }, "android": { + "permissions": ["CAMERA", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"], "adaptiveIcon": { "backgroundColor": "#E6F4FE", "foregroundImage": "./assets/android-icon-foreground.png", diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx index 99a9cda..8284e4e 100644 --- a/mobile/app/index.tsx +++ b/mobile/app/index.tsx @@ -47,7 +47,7 @@ export function HomeScreen() { item.id} + keyExtractor={(item) => item.name} contentContainerStyle={styles.list} /> diff --git a/mobile/app/scan.tsx b/mobile/app/scan.tsx index 76ecc9b..c365a65 100644 --- a/mobile/app/scan.tsx +++ b/mobile/app/scan.tsx @@ -1,20 +1,22 @@ import React from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native'; -import * as ImagePicker from 'react-native-image-picker'; +import * as ImagePicker from 'expo-image-picker'; export function ScanScreen() { const takePhoto = async () => { try { - const result = await ImagePicker.launchCamera({ - mediaType: 'photo', + const { status } = await ImagePicker.requestCameraPermissionsAsync(); + if (status !== 'granted') { + Alert.alert('Permission requise', "L'accès à la caméra est nécessaire pour scanner un document."); + return; + } + + const result = await ImagePicker.launchCameraAsync({ + mediaTypes: ['images'], quality: 1, }); - if (result.didCancel) return; - if (result.errorCode) { - Alert.alert('Erreur', result.errorMessage || "Impossible d'accéder à la caméra"); - return; - } + if (result.canceled) return; if (result.assets && result.assets[0]) { console.log('Photo taken:', result.assets[0]); diff --git a/mobile/app/upload.tsx b/mobile/app/upload.tsx index 0b4d31f..3031ba1 100644 --- a/mobile/app/upload.tsx +++ b/mobile/app/upload.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native'; -import * as ImagePicker from 'react-native-image-picker'; +import * as ImagePicker from 'expo-image-picker'; import { useUpload } from '../hooks/useUpload'; import { UploadProgress } from '../components/UploadProgress'; @@ -11,28 +11,30 @@ export function UploadScreen() { const pickImage = async () => { try { - const result = await ImagePicker.launchImageLibrary({ - mediaType: 'photo', - quality: 1, - }); - - if (result.didCancel) return; - if (result.errorCode) { - Alert.alert('Erreur', result.errorMessage || "Impossible d'accéder à la galerie"); + const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (status !== 'granted') { + Alert.alert('Permission requise', "L'accès à la galerie est nécessaire pour sélectionner une photo."); return; } + console.log('yes') + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ['images'], + quality: 1, + }); + console.log(result) + if (result.canceled) return; if (result.assets && result.assets[0]) { setUploadStatus('uploading'); const asset = result.assets[0]; const file = { uri: asset.uri, - type: asset.type || 'image/jpeg', + type: asset.mimeType || 'image/jpeg', name: asset.fileName || 'photo.jpg', }; try { - const response = await upload.mutateAsync(file as any); + const response = await upload.mutateAsync(file); setUploadStatus('processing'); console.log('Upload success:', response); setUploadStatus('success'); diff --git a/mobile/components/FileCard.tsx b/mobile/components/FileCard.tsx index b036d67..c9c21f0 100644 --- a/mobile/components/FileCard.tsx +++ b/mobile/components/FileCard.tsx @@ -15,6 +15,8 @@ export function FileCard({ file, onPress }: FileCardProps) { return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; + console.log(file) + return ( onPress?.(file)}> diff --git a/mobile/constants/api.ts b/mobile/constants/api.ts index 901cc3b..b6e2c44 100644 --- a/mobile/constants/api.ts +++ b/mobile/constants/api.ts @@ -1,4 +1,4 @@ -export const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://localhost:8080/api/v1'; +export const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://192.168.1.142:8080/api/v1'; export const ENDPOINTS = { FILES: '/files', diff --git a/mobile/hooks/useUpload.ts b/mobile/hooks/useUpload.ts index 9575b42..e6dfa08 100644 --- a/mobile/hooks/useUpload.ts +++ b/mobile/hooks/useUpload.ts @@ -1,20 +1,23 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { File, UploadType } from 'expo-file-system'; import { apiClient } from '../api/client'; -import { ENDPOINTS } from '../constants/api'; +import { API_BASE_URL, ENDPOINTS } from '../constants/api'; import { OcrJob } from '../types'; export function useUpload() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (file: File) => { - const formData = new FormData(); - formData.append('file', file); + mutationFn: async (file: { uri: string; type: string; name: string }) => { + const fsFile = new File(file.uri); + const result = await fsFile.upload(`${API_BASE_URL}${ENDPOINTS.UPLOAD}`, { + httpMethod: 'POST', + uploadType: UploadType.MULTIPART, + fieldName: 'file', + mimeType: file.type, + }); - return apiClient.uploadFile<{ id: string; ocrJobId: string }>( - ENDPOINTS.UPLOAD, - formData - ); + return JSON.parse(result.body) as { id: string; ocrJobId: string }; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['files'] }); diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 3abea2f..3b0a2b3 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -12,10 +12,11 @@ "@react-navigation/native-stack": "^7.17.10", "@tanstack/react-query": "^5.101.2", "expo": "~57.0.4", + "expo-file-system": "~57.0.0", + "expo-image-picker": "~57.0.2", "expo-status-bar": "~57.0.0", "react": "19.2.3", "react-native": "0.86.0", - "react-native-image-picker": "^8.2.1", "react-native-mmkv": "^4.3.2", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2" @@ -2797,6 +2798,37 @@ } } }, + "node_modules/expo-file-system": { + "version": "57.0.0", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.0.tgz", + "integrity": "sha512-G+eytNuNGMiS7dbdj1j0nAYMowXnNr1vpO+jss6nQvBlRxshcGBFMtk8xfc67ynz0MUVkzod7WwKO7Vf1iOaJw==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-image-loader": { + "version": "57.0.0", + "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-57.0.0.tgz", + "integrity": "sha512-EhwnoPC4T/EMdB7Nsg7qITzhO/qB0hUnmVghmtAKQwwDVaLWWYCGE4NLkes3zk9Ub451SmS/swgPp4PCPzOlnw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-image-picker": { + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-57.0.2.tgz", + "integrity": "sha512-XW+C5weIthkOLCJZzExeRJf9pcWfaXNHSDm76gmZVhDUVIHdi9IS3eihAIF0WA0fBo9ogIfQs/iep/c6CzIMKg==", + "license": "MIT", + "dependencies": { + "expo-image-loader": "~57.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "57.0.5", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.5.tgz", @@ -3177,16 +3209,6 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/expo-file-system": { - "version": "57.0.0", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.0.tgz", - "integrity": "sha512-G+eytNuNGMiS7dbdj1j0nAYMowXnNr1vpO+jss6nQvBlRxshcGBFMtk8xfc67ynz0MUVkzod7WwKO7Vf1iOaJw==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, "node_modules/expo/node_modules/expo-font": { "version": "57.0.0", "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.0.tgz", @@ -5225,16 +5247,6 @@ } } }, - "node_modules/react-native-image-picker": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-8.2.1.tgz", - "integrity": "sha512-FBeGYJGFDjMdGCcyubDJgBAPCQ4L1D3hwLXyUU91jY9ahOZMTbluceVvRmrEKqnDPFJ0gF1NVhJ0nr1nROFLdg==", - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-mmkv": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-4.3.2.tgz", diff --git a/mobile/package.json b/mobile/package.json index 9e803a0..2479087 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -7,10 +7,11 @@ "@react-navigation/native-stack": "^7.17.10", "@tanstack/react-query": "^5.101.2", "expo": "~57.0.4", + "expo-file-system": "~57.0.0", + "expo-image-picker": "~57.0.2", "expo-status-bar": "~57.0.0", "react": "19.2.3", "react-native": "0.86.0", - "react-native-image-picker": "^8.2.1", "react-native-mmkv": "^4.3.2", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2"