Bỏ qua để đến nội dung chính

Xác thực tùy chỉnh

Tạo các quy tắc xác thực tinh vi cho việc tải lên của bạn.

Quy tắc tùy chỉnh cơ bản

import { useDropup } from '@samithahansaka/dropup';

function CustomValidationUploader() {
const { files, getDropProps, getInputProps } = useDropup({
customRules: [
// Quy tắc 1: Kiểm tra tên tệp
(file) => {
if (file.name.includes(' ')) {
return 'Tên tệp không được chứa khoảng trắng';
}
return true;
},

// Quy tắc 2: Kiểm tra phần mở rộng
(file) => {
const ext = file.name.split('.').pop()?.toLowerCase();
const blocked = ['exe', 'bat', 'cmd', 'sh'];
if (ext && blocked.includes(ext)) {
return 'Không cho phép tệp thực thi';
}
return true;
},
],

onValidationError: (errors) => {
errors.forEach(({ file, errors }) => {
alert(`${file.name}:\n${errors.join('\n')}`);
});
},
});

return (
<div {...getDropProps()} style={styles.dropzone}>
<input {...getInputProps()} />
<p>Thả tệp vào đây (không có khoảng trắng trong tên, không có tệp thực thi)</p>
</div>
);
}

Xác thực bất đồng bộ

import { useDropup } from '@samithahansaka/dropup';

function AsyncValidationUploader() {
const { files, getDropProps, getInputProps } = useDropup({
customRules: [
// Kiểm tra trùng lặp trên máy chủ
async (file) => {
const hash = await calculateHash(file);
const response = await fetch('/api/check-duplicate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hash, filename: file.name }),
});
const { exists } = await response.json();

if (exists) {
return 'Tệp này đã được tải lên';
}
return true;
},

// Xác thực nội dung tệp
async (file) => {
if (file.type === 'application/json') {
const text = await file.text();
try {
JSON.parse(text);
return true;
} catch {
return 'Tệp JSON không hợp lệ';
}
}
return true;
},
],
});

return (
<div {...getDropProps()} style={styles.dropzone}>
<input {...getInputProps()} />
<p>Thả tệp vào đây (xác thực với máy chủ)</p>
</div>
);
}

// Hàm trợ giúp để tính hash tệp
async function calculateHash(file: File): Promise<string> {
const buffer = await file.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

Xác thực nội dung hình ảnh

import { useDropup } from '@samithahansaka/dropup';

function ImageContentValidator() {
const { files, getDropProps, getInputProps } = useDropup({
accept: 'image/*',

customRules: [
// Kiểm tra kích thước hình ảnh thực tế
async (file) => {
if (!file.type.startsWith('image/')) return true;

const dimensions = await getImageDimensions(file);

if (dimensions.width < 200 || dimensions.height < 200) {
return 'Hình ảnh phải có kích thước tối thiểu 200x200 pixel';
}

if (dimensions.width > 4000 || dimensions.height > 4000) {
return 'Hình ảnh không được vượt quá 4000x4000 pixel';
}

return true;
},

// Kiểm tra tỷ lệ khung hình
async (file) => {
if (!file.type.startsWith('image/')) return true;

const { width, height } = await getImageDimensions(file);
const ratio = width / height;

// Yêu cầu hình ảnh gần vuông (tỷ lệ 0.8 đến 1.2)
if (ratio < 0.8 || ratio > 1.2) {
return 'Hình ảnh phải gần vuông (tỷ lệ khung hình 0.8-1.2)';
}

return true;
},
],
});

return (
<div {...getDropProps()} style={styles.dropzone}>
<input {...getInputProps()} />
<p>Thả hình ảnh vuông (tối thiểu 200px, tối đa 4000px)</p>

<div style={styles.gallery}>
{files.map(file => (
<img
key={file.id}
src={file.preview}
alt=""
style={styles.preview}
/>
))}
</div>
</div>
);
}

// Hàm trợ giúp để lấy kích thước hình ảnh
function getImageDimensions(file: File): Promise<{ width: number; height: number }> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
resolve({ width: img.width, height: img.height });
URL.revokeObjectURL(img.src);
};
img.onerror = reject;
img.src = URL.createObjectURL(file);
});
}

const styles = {
dropzone: {
border: '2px dashed #ccc',
borderRadius: 8,
padding: 40,
textAlign: 'center' as const,
},
gallery: {
display: 'flex',
gap: 8,
marginTop: 16,
justifyContent: 'center',
},
preview: {
width: 80,
height: 80,
objectFit: 'cover' as const,
borderRadius: 4,
},
};

Xác thực với điều kiện phụ thuộc

import { useDropup } from '@samithahansaka/dropup';
import { useState } from 'react';

function ConditionalValidation() {
const [category, setCategory] = useState('image');

const { files, getDropProps, getInputProps } = useDropup({
// Accept động dựa trên danh mục
accept: category === 'image'
? 'image/*'
: category === 'document'
? '.pdf,.doc,.docx'
: '*/*',

customRules: [
(file) => {
// Xác thực theo danh mục cụ thể
if (category === 'image') {
if (!file.type.startsWith('image/')) {
return 'Vui lòng tải lên tệp hình ảnh';
}
if (file.size > 5 * 1024 * 1024) {
return 'Hình ảnh phải dưới 5MB';
}
}

if (category === 'document') {
if (file.size > 10 * 1024 * 1024) {
return 'Tài liệu phải dưới 10MB';
}
}

return true;
},
],
});

return (
<div>
<div style={{ marginBottom: 20 }}>
<label>
<input
type="radio"
value="image"
checked={category === 'image'}
onChange={() => setCategory('image')}
/>
Hình ảnh (tối đa 5MB)
</label>
<label style={{ marginLeft: 20 }}>
<input
type="radio"
value="document"
checked={category === 'document'}
onChange={() => setCategory('document')}
/>
Tài liệu (tối đa 10MB)
</label>
</div>

<div {...getDropProps()} style={styles.dropzone}>
<input {...getInputProps()} />
<p>
Thả {category === 'image' ? 'hình ảnh' : 'tài liệu'} vào đây
</p>
</div>
</div>
);
}

Quét virus/Malware

import { useDropup } from '@samithahansaka/dropup';

function MalwareScanUploader() {
const { files, state, actions, getDropProps, getInputProps } = useDropup({
customRules: [
// Quét tệp với dịch vụ bên ngoài
async (file) => {
// Tải lên dịch vụ quét virus
const formData = new FormData();
formData.append('file', file);

try {
const response = await fetch('/api/scan', {
method: 'POST',
body: formData,
});

const { safe, threat } = await response.json();

if (!safe) {
return `Phát hiện mã độc: ${threat}`;
}

return true;
} catch (error) {
return 'Không thể quét tệp. Vui lòng thử lại.';
}
},
],

onValidationError: (errors) => {
errors.forEach(({ file, errors }) => {
console.error(`${file.name} bị từ chối:`, errors);
});
},
});

return (
<div {...getDropProps()} style={styles.dropzone}>
<input {...getInputProps()} />
<p>Tệp được quét mã độc trước khi tải lên</p>
</div>
);
}

Quy tắc xác thực có sẵn

import { useDropup, commonRules } from '@samithahansaka/dropup';

function PrebuiltRulesUploader() {
const { files, getDropProps, getInputProps } = useDropup({
customRules: [
// Quy tắc có sẵn
commonRules.noExecutables, // Chặn .exe, .bat, v.v.
commonRules.noHiddenFiles, // Chặn tệp bắt đầu bằng .
commonRules.maxFilenameLength(50), // Tối đa 50 ký tự
commonRules.allowedExtensions(['.jpg', '.png', '.pdf']),

// Kết hợp với quy tắc tùy chỉnh
(file) => {
if (file.name.toLowerCase().includes('temp')) {
return 'Không cho phép tệp tạm thời';
}
return true;
},
],
});

return (
<div {...getDropProps()} style={styles.dropzone}>
<input {...getInputProps()} />
<p>Xác thực tệp nghiêm ngặt được bật</p>
</div>
);
}

Hiển thị lỗi xác thực

import { useDropup } from '@samithahansaka/dropup';
import { useState } from 'react';

type ValidationErrorType = { file: File; errors: string[] };

function ValidationErrorDisplay() {
const [validationErrors, setValidationErrors] = useState<ValidationErrorType[]>([]);

const { files, getDropProps, getInputProps } = useDropup({
accept: 'image/*',
maxSize: 5 * 1024 * 1024,
maxFiles: 3,

customRules: [
(file) => {
if (file.name.length > 50) {
return 'Tên tệp quá dài (tối đa 50 ký tự)';
}
return true;
},
],

onValidationError: (errors) => {
setValidationErrors(errors);

// Xóa sau 5 giây
setTimeout(() => setValidationErrors([]), 5000);
},
});

return (
<div>
<div {...getDropProps()} style={styles.dropzone}>
<input {...getInputProps()} />
<p>Thả tối đa 3 hình ảnh (mỗi tệp tối đa 5MB)</p>
</div>

{/* Lỗi xác thực */}
{validationErrors.length > 0 && (
<div style={styles.errorContainer}>
<h4 style={styles.errorTitle}>Một số tệp bị từ chối:</h4>
{validationErrors.map(({ file, errors }, index) => (
<div key={index} style={styles.errorItem}>
<strong>{file.name}</strong>
<ul style={styles.errorList}>
{errors.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
))}
</div>
)}

{/* Tệp được chấp nhận */}
{files.length > 0 && (
<div style={styles.acceptedFiles}>
<h4>Các tệp được chấp nhận:</h4>
{files.map(file => (
<div key={file.id}>{file.name}</div>
))}
</div>
)}
</div>
);
}

const styles = {
dropzone: {
border: '2px dashed #ccc',
borderRadius: 8,
padding: 40,
textAlign: 'center' as const,
},
errorContainer: {
marginTop: 16,
padding: 16,
backgroundColor: '#ffebee',
borderRadius: 8,
border: '1px solid #f44336',
},
errorTitle: {
color: '#c62828',
margin: '0 0 12px',
},
errorItem: {
marginBottom: 8,
},
errorList: {
margin: '4px 0 0',
paddingLeft: 20,
color: '#c62828',
},
acceptedFiles: {
marginTop: 16,
padding: 16,
backgroundColor: '#e8f5e9',
borderRadius: 8,
},
};