upload file is working
This commit is contained in:
@@ -1,19 +1,69 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListFiles(c *gin.Context) {
|
func ListFiles(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
|
dirs, err := os.ReadDir("./uploads/")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
fmt.Println("What")
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"data": []interface{}{},
|
"data": []interface{}{},
|
||||||
"meta": gin.H{
|
"meta": gin.H{
|
||||||
"page": 1,
|
"page": 1,
|
||||||
"total": 0,
|
"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": files,
|
||||||
|
"meta": gin.H{
|
||||||
|
"page": 1,
|
||||||
|
"total": 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFile(c *gin.Context) {
|
func GetFile(c *gin.Context) {
|
||||||
@@ -26,6 +76,24 @@ func GetFile(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UploadFile(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{
|
c.JSON(http.StatusNotImplemented, gin.H{
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
"code": "NOT_IMPLEMENTED",
|
"code": "NOT_IMPLEMENTED",
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UploadFile(f *multipart.FileHeader) {
|
||||||
|
fmt.Println(f.Filename)
|
||||||
|
}
|
||||||
@@ -43,24 +43,6 @@ class ApiClient {
|
|||||||
async delete<T>(endpoint: string): Promise<T> {
|
async delete<T>(endpoint: string): Promise<T> {
|
||||||
return this.request<T>(endpoint, { method: 'DELETE' });
|
return this.request<T>(endpoint, { method: 'DELETE' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile<T>(endpoint: string, formData: FormData): Promise<T> {
|
|
||||||
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);
|
export const apiClient = new ApiClient(API_BASE_URL);
|
||||||
|
|||||||
+6
-1
@@ -7,9 +7,14 @@
|
|||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "light",
|
"userInterfaceStyle": "light",
|
||||||
"ios": {
|
"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": {
|
"android": {
|
||||||
|
"permissions": ["CAMERA", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"],
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"backgroundColor": "#E6F4FE",
|
"backgroundColor": "#E6F4FE",
|
||||||
"foregroundImage": "./assets/android-icon-foreground.png",
|
"foregroundImage": "./assets/android-icon-foreground.png",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function HomeScreen() {
|
|||||||
<FlatList
|
<FlatList
|
||||||
data={data?.data || []}
|
data={data?.data || []}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.name}
|
||||||
contentContainerStyle={styles.list}
|
contentContainerStyle={styles.list}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -1,20 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
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() {
|
export function ScanScreen() {
|
||||||
const takePhoto = async () => {
|
const takePhoto = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await ImagePicker.launchCamera({
|
const { status } = await ImagePicker.requestCameraPermissionsAsync();
|
||||||
mediaType: 'photo',
|
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,
|
quality: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.didCancel) return;
|
if (result.canceled) return;
|
||||||
if (result.errorCode) {
|
|
||||||
Alert.alert('Erreur', result.errorMessage || "Impossible d'accéder à la caméra");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.assets && result.assets[0]) {
|
if (result.assets && result.assets[0]) {
|
||||||
console.log('Photo taken:', result.assets[0]);
|
console.log('Photo taken:', result.assets[0]);
|
||||||
|
|||||||
+13
-11
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
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 { useUpload } from '../hooks/useUpload';
|
||||||
import { UploadProgress } from '../components/UploadProgress';
|
import { UploadProgress } from '../components/UploadProgress';
|
||||||
|
|
||||||
@@ -11,28 +11,30 @@ export function UploadScreen() {
|
|||||||
|
|
||||||
const pickImage = async () => {
|
const pickImage = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await ImagePicker.launchImageLibrary({
|
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||||
mediaType: 'photo',
|
if (status !== 'granted') {
|
||||||
quality: 1,
|
Alert.alert('Permission requise', "L'accès à la galerie est nécessaire pour sélectionner une photo.");
|
||||||
});
|
|
||||||
|
|
||||||
if (result.didCancel) return;
|
|
||||||
if (result.errorCode) {
|
|
||||||
Alert.alert('Erreur', result.errorMessage || "Impossible d'accéder à la galerie");
|
|
||||||
return;
|
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]) {
|
if (result.assets && result.assets[0]) {
|
||||||
setUploadStatus('uploading');
|
setUploadStatus('uploading');
|
||||||
const asset = result.assets[0];
|
const asset = result.assets[0];
|
||||||
const file = {
|
const file = {
|
||||||
uri: asset.uri,
|
uri: asset.uri,
|
||||||
type: asset.type || 'image/jpeg',
|
type: asset.mimeType || 'image/jpeg',
|
||||||
name: asset.fileName || 'photo.jpg',
|
name: asset.fileName || 'photo.jpg',
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await upload.mutateAsync(file as any);
|
const response = await upload.mutateAsync(file);
|
||||||
setUploadStatus('processing');
|
setUploadStatus('processing');
|
||||||
console.log('Upload success:', response);
|
console.log('Upload success:', response);
|
||||||
setUploadStatus('success');
|
setUploadStatus('success');
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export function FileCard({ file, onPress }: FileCardProps) {
|
|||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(file)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.container} onPress={() => onPress?.(file)}>
|
<TouchableOpacity style={styles.container} onPress={() => onPress?.(file)}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
|
|||||||
@@ -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 = {
|
export const ENDPOINTS = {
|
||||||
FILES: '/files',
|
FILES: '/files',
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { File, UploadType } from 'expo-file-system';
|
||||||
import { apiClient } from '../api/client';
|
import { apiClient } from '../api/client';
|
||||||
import { ENDPOINTS } from '../constants/api';
|
import { API_BASE_URL, ENDPOINTS } from '../constants/api';
|
||||||
import { OcrJob } from '../types';
|
import { OcrJob } from '../types';
|
||||||
|
|
||||||
export function useUpload() {
|
export function useUpload() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (file: File) => {
|
mutationFn: async (file: { uri: string; type: string; name: string }) => {
|
||||||
const formData = new FormData();
|
const fsFile = new File(file.uri);
|
||||||
formData.append('file', file);
|
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 }>(
|
return JSON.parse(result.body) as { id: string; ocrJobId: string };
|
||||||
ENDPOINTS.UPLOAD,
|
|
||||||
formData
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||||
|
|||||||
Generated
+33
-21
@@ -12,10 +12,11 @@
|
|||||||
"@react-navigation/native-stack": "^7.17.10",
|
"@react-navigation/native-stack": "^7.17.10",
|
||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"expo": "~57.0.4",
|
"expo": "~57.0.4",
|
||||||
|
"expo-file-system": "~57.0.0",
|
||||||
|
"expo-image-picker": "~57.0.2",
|
||||||
"expo-status-bar": "~57.0.0",
|
"expo-status-bar": "~57.0.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-native": "0.86.0",
|
"react-native": "0.86.0",
|
||||||
"react-native-image-picker": "^8.2.1",
|
|
||||||
"react-native-mmkv": "^4.3.2",
|
"react-native-mmkv": "^4.3.2",
|
||||||
"react-native-safe-area-context": "~5.7.0",
|
"react-native-safe-area-context": "~5.7.0",
|
||||||
"react-native-screens": "4.25.2"
|
"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": {
|
"node_modules/expo-modules-autolinking": {
|
||||||
"version": "57.0.5",
|
"version": "57.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-57.0.5.tgz",
|
||||||
@@ -3177,16 +3209,6 @@
|
|||||||
"react-native": "*"
|
"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": {
|
"node_modules/expo/node_modules/expo-font": {
|
||||||
"version": "57.0.0",
|
"version": "57.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.0.tgz",
|
"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": {
|
"node_modules/react-native-mmkv": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-4.3.2.tgz",
|
||||||
|
|||||||
+2
-1
@@ -7,10 +7,11 @@
|
|||||||
"@react-navigation/native-stack": "^7.17.10",
|
"@react-navigation/native-stack": "^7.17.10",
|
||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"expo": "~57.0.4",
|
"expo": "~57.0.4",
|
||||||
|
"expo-file-system": "~57.0.0",
|
||||||
|
"expo-image-picker": "~57.0.2",
|
||||||
"expo-status-bar": "~57.0.0",
|
"expo-status-bar": "~57.0.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-native": "0.86.0",
|
"react-native": "0.86.0",
|
||||||
"react-native-image-picker": "^8.2.1",
|
|
||||||
"react-native-mmkv": "^4.3.2",
|
"react-native-mmkv": "^4.3.2",
|
||||||
"react-native-safe-area-context": "~5.7.0",
|
"react-native-safe-area-context": "~5.7.0",
|
||||||
"react-native-screens": "4.25.2"
|
"react-native-screens": "4.25.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user