Initial commit
This commit is contained in:
168
.claude/skills/pocketbase-best-practices/rules/file-upload.md
Normal file
168
.claude/skills/pocketbase-best-practices/rules/file-upload.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
title: Upload Files Correctly
|
||||
impact: MEDIUM
|
||||
impactDescription: Reliable uploads with progress tracking and validation
|
||||
tags: files, upload, storage, attachments
|
||||
---
|
||||
|
||||
## Upload Files Correctly
|
||||
|
||||
File uploads can use plain objects or FormData. Handle large files properly with progress tracking and appropriate error handling.
|
||||
|
||||
**Incorrect (naive file upload):**
|
||||
|
||||
```javascript
|
||||
// Missing error handling
|
||||
async function uploadFile(file) {
|
||||
await pb.collection('documents').create({
|
||||
title: file.name,
|
||||
file: file
|
||||
});
|
||||
// No error handling, no progress feedback
|
||||
}
|
||||
|
||||
// Uploading without validation
|
||||
async function uploadAvatar(file) {
|
||||
await pb.collection('users').update(userId, {
|
||||
avatar: file // No size/type check - might fail server-side
|
||||
});
|
||||
}
|
||||
|
||||
// Base64 upload (inefficient)
|
||||
async function uploadImage(base64) {
|
||||
await pb.collection('images').create({
|
||||
image: base64 // Wrong! PocketBase expects File/Blob
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (proper file uploads):**
|
||||
|
||||
```javascript
|
||||
// Basic upload with object (auto-converts to FormData)
|
||||
async function uploadDocument(file, metadata) {
|
||||
try {
|
||||
const record = await pb.collection('documents').create({
|
||||
title: metadata.title,
|
||||
description: metadata.description,
|
||||
file: file // File object from input
|
||||
});
|
||||
return record;
|
||||
} catch (error) {
|
||||
if (error.response?.data?.file) {
|
||||
throw new Error(`File error: ${error.response.data.file.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Upload multiple files
|
||||
async function uploadGallery(files, albumId) {
|
||||
const record = await pb.collection('albums').update(albumId, {
|
||||
images: files // Array of File objects
|
||||
});
|
||||
return record;
|
||||
}
|
||||
|
||||
// FormData for more control
|
||||
async function uploadWithProgress(file, onProgress) {
|
||||
const formData = new FormData();
|
||||
formData.append('title', file.name);
|
||||
formData.append('file', file);
|
||||
|
||||
// Using fetch directly for progress (SDK doesn't expose progress)
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
onProgress(Math.round((e.loaded / e.total) * 100));
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(JSON.parse(xhr.responseText));
|
||||
} else {
|
||||
reject(new Error(`Upload failed: ${xhr.status}`));
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => reject(new Error('Upload failed')));
|
||||
|
||||
xhr.open('POST', `${pb.baseURL}/api/collections/documents/records`);
|
||||
xhr.setRequestHeader('Authorization', pb.authStore.token);
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
// Client-side validation before upload
|
||||
function validateFile(file, options = {}) {
|
||||
const {
|
||||
maxSize = 10 * 1024 * 1024, // 10MB default
|
||||
allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'],
|
||||
maxNameLength = 100
|
||||
} = options;
|
||||
|
||||
const errors = [];
|
||||
|
||||
if (file.size > maxSize) {
|
||||
errors.push(`File too large. Max: ${maxSize / 1024 / 1024}MB`);
|
||||
}
|
||||
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
errors.push(`Invalid file type: ${file.type}`);
|
||||
}
|
||||
|
||||
if (file.name.length > maxNameLength) {
|
||||
errors.push(`Filename too long`);
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
// Complete upload flow
|
||||
async function handleFileUpload(inputEvent) {
|
||||
const file = inputEvent.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate
|
||||
const validation = validateFile(file, {
|
||||
maxSize: 5 * 1024 * 1024,
|
||||
allowedTypes: ['image/jpeg', 'image/png']
|
||||
});
|
||||
|
||||
if (!validation.valid) {
|
||||
showError(validation.errors.join(', '));
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload with progress
|
||||
try {
|
||||
setUploading(true);
|
||||
const record = await uploadWithProgress(file, setProgress);
|
||||
showSuccess('Upload complete!');
|
||||
return record;
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Deleting files:**
|
||||
|
||||
```javascript
|
||||
// Remove specific file(s) from record
|
||||
await pb.collection('albums').update(albumId, {
|
||||
'images-': ['filename1.jpg', 'filename2.jpg'] // Remove these files
|
||||
});
|
||||
|
||||
// Clear all files
|
||||
await pb.collection('documents').update(docId, {
|
||||
file: null // Removes the file
|
||||
});
|
||||
```
|
||||
|
||||
Reference: [PocketBase File Upload](https://pocketbase.io/docs/files-handling/)
|
||||
Reference in New Issue
Block a user