diff --git a/next.config.ts b/next.config.ts index 3e44d22..764f595 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,7 +2,11 @@ import type { NextConfig } from "next"; import createNextIntlPlugin from 'next-intl/plugin'; const nextConfig: NextConfig = { - /* output: 'export' */ + experimental: { + serverActions: { + bodySizeLimit: '6mb', + }, + }, }; const withNextIntl = createNextIntlPlugin(); diff --git a/package.json b/package.json index 08a9141..7a8f998 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "no-copy-frontend", - "version": "0.7.0", + "version": "0.8.0", "private": true, "scripts": { "dev": "next dev -p 2999", diff --git a/src/app/[locale]/pages/marking-photo/page.tsx b/src/app/[locale]/pages/marking-photo/page.tsx index 1c1b711..677a7ed 100644 --- a/src/app/[locale]/pages/marking-photo/page.tsx +++ b/src/app/[locale]/pages/marking-photo/page.tsx @@ -1,7 +1,7 @@ import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table'; import ProtectionSummary from '@/app/ui/marking-page/protection-summary'; import TestSection from '@/app/ui/marking-page/test-section'; -import UploadSectionImage from '@/app/ui/marking-page/upload-section-image'; +import UploadSectionFile from '@/app/components/upload-section-file'; import { useTranslations } from 'next-intl'; import FilesTable from '@/app/ui/dashboard/files-table'; @@ -15,7 +15,7 @@ export default function Page() { - + {/* */} {/* */} diff --git a/src/app/actions/file-upload.ts b/src/app/actions/file-upload.ts new file mode 100644 index 0000000..1b95387 --- /dev/null +++ b/src/app/actions/file-upload.ts @@ -0,0 +1,146 @@ +'use server' + +import { localDevelopmentUrl } from '@/app/actions/definitions'; +import { getSessionData, createSession } from '@/app/actions/session'; +import { number } from 'zod'; + +const API_BASE_URL = process.env.NODE_ENV === 'development' + ? localDevelopmentUrl + : 'http://app:8080'; + +interface initMessageBody { + file_name: string, + file_type: string, + extension: string, + file_size: number | string, + action?: string, + token?: string +} + +export async function fileUpload(messageBody: initMessageBody) { + const token = await getSessionData('token'); + if (!token) { + return; + } + const message = messageBody; + message.action = "init"; + message.token = token; + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 20004, + message_body: message + }), + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }); + + if (response.ok) { + let parsed = await response.json() as { + message_desc: string, + message_body: { + upload_id: string, + total_chunks: number, + chunk_size: number, + status: string + }, + message_code: string + }; + console.log(parsed); + + if (parsed.message_desc === 'Operation successful') { + return parsed.message_body; + } else { + console.log('error fileUpload'); + throw parsed; + } + } else { + throw (`${response.status}`); + } + } catch (error) { + return error; + } +} + +export async function cancelUpload(udloadId: string) { + console.log('cancelUpload'); + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 20004, + message_body: { + action: "cancel", + upload_id: udloadId, + } + }), + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }); + + if (response.ok) { + let parsed = await response.json() as { + message_code: string, + message_desc: string, + message_body: { + upload_id: string, + message: string + } + }; + + console.log(parsed); + + if (parsed.message_desc === 'Upload cancelled successfully') { + return parsed.message_body; + } else { + console.log('error cancel upload'); + throw parsed; + } + } else { + throw (`${response.status}`); + } + } catch (error) { + return error; + } +} + +export async function chunkUpload(formData: FormData) { + console.log('chunkUpload'); + console.log(`${API_BASE_URL}/api/v1/files/chunk`); + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/files/chunk`, { + method: 'POST', + body: formData + }); + + console.log(response); + + if (response.ok) { + let parsed = await response.json(); + console.log('response.ok'); + console.log(parsed); + + if (parsed.message_desc === 'Upload cancelled successfully') { + return parsed.message_body; + } else { + console.log('error cancel chunkUpload'); + throw parsed; + } + } else { + throw (`${response.status}`); + } + + } catch (error) { + return error + } +} \ No newline at end of file diff --git a/src/app/actions/file-uploads.ts b/src/app/actions/file-uploads.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/components/upload-section-file.tsx b/src/app/components/upload-section-file.tsx new file mode 100644 index 0000000..89e1f40 --- /dev/null +++ b/src/app/components/upload-section-file.tsx @@ -0,0 +1,355 @@ +'use client' + +import React, { useRef, useState, useCallback, ChangeEvent, DragEvent } from 'react'; +import { IconShieldAdd } from '@/app/ui/icons/icons'; +import { fileUpload, cancelUpload, chunkUpload } from '@/app/actions/file-upload'; + +interface SelectedFile { + file: File; + preview: string; + name: string; + size: string; + dimensions?: { + width: number; + height: number; + }; +} + +interface FileUploadInitResponse { + upload_id: string, + file_name: string, + total_chunks: number, + chunk_size: number, + status: string +} + +type SupportedFormat = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/bmp'; +type FileExtension = 'jpg' | 'jpeg' | 'png' | 'gif' | 'bmp'; + +export default function UploadSectionFile({ fileType }: { fileType: string }) { + const fileInputRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [error, setError] = useState(null); + const [uploadId, setUploadId] = useState(null); + + const SUPPORTED_FORMATS: SupportedFormat[] = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/bmp' + ]; + + const ALLOWED_EXTENSIONS: FileExtension[] = ['jpg', 'jpeg', 'png', 'gif', 'bmp']; + + + const validateFile = (file: File): { isValid: boolean; errorMessage?: string } => { + if (!SUPPORTED_FORMATS.includes(file.type as SupportedFormat)) { + const extension = file.name.split('.').pop()?.toLowerCase(); + if (!extension || !ALLOWED_EXTENSIONS.includes(extension as FileExtension)) { + return { + isValid: false, + errorMessage: 'Неподдерживаемый формат файла.' + }; + } + } + + const MAX_SIZE = 100 * 1024 * 1024; + if (file.size > MAX_SIZE) { + return { + isValid: false, + errorMessage: 'Файл слишком большой. Максимальный размер: 100MB' + }; + } + + return { isValid: true }; + }; + + const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => { + return new Promise((resolve, reject) => { + const img = new Image(); + const objectUrl = URL.createObjectURL(file); + + img.onload = () => { + const dimensions = { + width: img.width, + height: img.height + }; + URL.revokeObjectURL(objectUrl); + resolve(dimensions); + }; + + img.onerror = () => { + URL.revokeObjectURL(objectUrl); + reject(new Error('Не удалось загрузить фаил')); + }; + + img.src = objectUrl; + }); + }; + + const handleFileSelect = useCallback(async (file: File | null): Promise => { + if (!file) { + setError(null); + return; + } + + setError(null); + + const validation = validateFile(file); + + if (!validation.isValid) { + setError(validation.errorMessage || 'Неизвестная ошибка валидации'); + return; + } + + try { + const dimensions = await getImageDimensions(file); + + if (dimensions.width < 150 || dimensions.height < 150 || + dimensions.width > 8000 || dimensions.height > 8000) { + setError(`Размер изображения: ${dimensions.width}x${dimensions.height}. Допустимый диапазон: 150x150 - 4000x4000 пикселей.`); + return; + } + + const objectUrl = URL.createObjectURL(file); + + setSelectedFile({ + file, + preview: objectUrl, + name: file.name, + size: `${(file.size / 1024 / 1024).toFixed(2)} MB`, + dimensions + }); + + console.log('Файл обработан:', { + name: file.name, + type: file.type, + size: file.size, + dimensions + }); + + } catch (err) { + setError(err instanceof Error ? err.message : 'Ошибка при обработке файла'); + } + }, []); + + const handleFileInputChange = (event: ChangeEvent): void => { + const file = event.target.files?.[0] || null; + handleFileSelect(file); + + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const handleButtonClick = (): void => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const handleDragOver = (event: DragEvent): void => { + event.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (): void => { + setIsDragging(false); + }; + + const handleDrop = (event: DragEvent): void => { + event.preventDefault(); + setIsDragging(false); + + const file = event.dataTransfer.files[0]; + handleFileSelect(file); + }; + + const handleClearFile = async (): Promise => { + if (selectedFile) { + URL.revokeObjectURL(selectedFile.preview); + } + + const response = await cancelUpload(uploadId); + console.log(response); + + setSelectedFile(null); + setError(null); + }; + + const handlerFileUpload = async (file: SelectedFile): Promise => { + const initMessageBody = { + file_name: file.name, + file_type: fileType, + extension: file.name.split('.')[1], + file_size: file.file.size + }; + console.log("initMessageBody"); + + const response = await fileUpload(initMessageBody) as FileUploadInitResponse; + + if (response?.upload_id) { + setUploadId(response.upload_id); + console.log(response); + console.log(file.file); + /* { + chunk_size: 5242880 + field_errors: null + file_name: "ooop.jpg" + status: "INITIATED" + total_chunks: 1 + upload_id: "623261f0-f13b-4d06-8df5-365cb19d7ada" + } */ + /* file.file */ + const CHUNK_SIZE = response.chunk_size; + const totalChunks = response.total_chunks; + const fileSize = file.file.size; + let start = 0; + let chunkIndex = 0; + + console.log('response.upload_id'); + console.log(response.upload_id); + + try { + while (start < fileSize) { + const end = Math.min(start + CHUNK_SIZE, fileSize); + const chunk = file.file.slice(start, end); + console.log(file.file); + console.log(chunk); + + const formData = new FormData(); + formData.append('upload_id', response.upload_id); + formData.append('chunk_number', chunkIndex.toString()); + formData.append('chunk', chunk); + const chunkResponse = await chunkUpload(formData); + + console.log(chunkResponse); + + if (chunkIndex === totalChunks - 1) { + console.log('upload done'); + } + + start = end; + chunkIndex++; + } + + console.log('File uploaded successfully'); + } catch (error) { + console.error('Upload error:', error); + } + } + } + + React.useEffect(() => { + return () => { + if (selectedFile) { + URL.revokeObjectURL(selectedFile.preview); + } + }; + }, [selectedFile]); + + return ( +
+

Загрузить изображение для защиты

+ +
+ +

Перетащите фаил сюда

+

или

+ + + + +

+ Разрешение изображение: 100x100 - 10000x10000 пикселей +

+

+ Размер файла: до 20 МБ +

+

+ Формат файла: JPG, PNG, GIF, BMP (WEBP НЕ поддерживается) +

+ + {error && ( +
+

❌ {error}

+
+ )} + + {selectedFile && ( +
+
+

+ Файл: {selectedFile.name} +

+

+ Размер: {selectedFile.size} +

+

+ Стоимость защиты: 0 +

+

+ Текущий баланс: 0 +

+
+ + Предпросмотр загруженного изображения setError('Не удалось загрузить превью изображения')} + /> + +
+ + +
+
+ )} +
+
+ ); +} \ No newline at end of file