add fileUpload functions
This commit is contained in:
+5
-1
@@ -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();
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 2999",
|
||||
|
||||
@@ -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() {
|
||||
</h1>
|
||||
</div>
|
||||
<ProtectionSummary />
|
||||
<UploadSectionImage />
|
||||
<UploadSectionFile fileType="IMAGE"/>
|
||||
{/* <TestSection /> */}
|
||||
<FilesTable />
|
||||
{/* <ProtectedFilesTable /> */}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [selectedFile, setSelectedFile] = useState<SelectedFile | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [uploadId, setUploadId] = useState<any>(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<void> => {
|
||||
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<HTMLInputElement>): 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<HTMLDivElement>): void => {
|
||||
event.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (): void => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (event: DragEvent<HTMLDivElement>): void => {
|
||||
event.preventDefault();
|
||||
setIsDragging(false);
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
handleFileSelect(file);
|
||||
};
|
||||
|
||||
const handleClearFile = async (): Promise<void> => {
|
||||
if (selectedFile) {
|
||||
URL.revokeObjectURL(selectedFile.preview);
|
||||
}
|
||||
|
||||
const response = await cancelUpload(uploadId);
|
||||
console.log(response);
|
||||
|
||||
setSelectedFile(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handlerFileUpload = async (file: SelectedFile): Promise<any> => {
|
||||
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 (
|
||||
<div className="upload-section">
|
||||
<h3>Загрузить изображение для защиты</h3>
|
||||
|
||||
<div
|
||||
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
style={{
|
||||
border: isDragging ? '2px dashed #3b82f6' : '2px dashed #d1d5db',
|
||||
backgroundColor: isDragging ? '#eff6ff' : '#f9fafb',
|
||||
padding: '2rem',
|
||||
borderRadius: '.75rem',
|
||||
textAlign: 'center',
|
||||
transition: 'all .2s ease-in-out'
|
||||
}}
|
||||
>
|
||||
<IconShieldAdd />
|
||||
<h4>Перетащите фаил сюда</h4>
|
||||
<p className="mb-2.5 text-[#6b7280]">или</p>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.gif,.bmp"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileInputChange}
|
||||
aria-label="Выбор файла для защиты"
|
||||
/>
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleButtonClick}
|
||||
type="button"
|
||||
>
|
||||
Выбрать файлы для защиты
|
||||
</button>
|
||||
|
||||
<p className="description">
|
||||
Разрешение изображение: 100x100 - 10000x10000 пикселей
|
||||
</p>
|
||||
<p className="description">
|
||||
Размер файла: до 20 МБ
|
||||
</p>
|
||||
<p className="description">
|
||||
Формат файла: JPG, PNG, GIF, BMP (WEBP НЕ поддерживается)
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-red-600 font-medium">❌ {error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedFile && (
|
||||
<div
|
||||
className="selected-file"
|
||||
>
|
||||
<div className="selected-file-file-info">
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">Файл:</span> {selectedFile.name}
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">Размер:</span> {selectedFile.size}
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">Стоимость защиты:</span> 0
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">Текущий баланс:</span> 0
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src={selectedFile.preview}
|
||||
alt="Предпросмотр загруженного изображения"
|
||||
className="uploaded-image"
|
||||
onError={() => setError('Не удалось загрузить превью изображения')}
|
||||
/>
|
||||
|
||||
<div className="mt-4 flex gap-3 justify-center">
|
||||
<button
|
||||
className="btn btn-cancel"
|
||||
onClick={handleClearFile}
|
||||
type="button"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-confirm"
|
||||
onClick={() => {
|
||||
handlerFileUpload(selectedFile);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Подтвердить защиту
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user