basic upload
This commit is contained in:
1
.prettierrc
Normal file
1
.prettierrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
example/A.csv
Normal file
1
example/A.csv
Normal file
@@ -0,0 +1 @@
|
|||||||
|
example
|
||||||
|
1
example/B.csv
Normal file
1
example/B.csv
Normal file
@@ -0,0 +1 @@
|
|||||||
|
other_file
|
||||||
|
1
example/imported/A_1.csv
Normal file
1
example/imported/A_1.csv
Normal file
@@ -0,0 +1 @@
|
|||||||
|
example
|
||||||
|
@@ -17,7 +17,8 @@
|
|||||||
"drizzle-orm": "^0.44.7",
|
"drizzle-orm": "^0.44.7",
|
||||||
"next": "16.0.6",
|
"next": "16.0.6",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0"
|
"react-dom": "19.2.0",
|
||||||
|
"react-dropzone": "^14.3.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -23,6 +23,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: 19.2.0
|
specifier: 19.2.0
|
||||||
version: 19.2.0(react@19.2.0)
|
version: 19.2.0(react@19.2.0)
|
||||||
|
react-dropzone:
|
||||||
|
specifier: ^14.3.8
|
||||||
|
version: 14.3.8(react@19.2.0)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4
|
specifier: ^4
|
||||||
@@ -1120,6 +1123,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
attr-accept@2.2.5:
|
||||||
|
resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
available-typed-arrays@1.0.7:
|
available-typed-arrays@1.0.7:
|
||||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1613,6 +1620,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
file-selector@2.1.2:
|
||||||
|
resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==}
|
||||||
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
file-uri-to-path@1.0.0:
|
file-uri-to-path@1.0.0:
|
||||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||||
|
|
||||||
@@ -2238,6 +2249,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.2.0
|
react: ^19.2.0
|
||||||
|
|
||||||
|
react-dropzone@14.3.8:
|
||||||
|
resolution: {integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==}
|
||||||
|
engines: {node: '>= 10.13'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>= 16.8 || 18.0.0'
|
||||||
|
|
||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
@@ -3495,6 +3512,8 @@ snapshots:
|
|||||||
|
|
||||||
async-function@1.0.0: {}
|
async-function@1.0.0: {}
|
||||||
|
|
||||||
|
attr-accept@2.2.5: {}
|
||||||
|
|
||||||
available-typed-arrays@1.0.7:
|
available-typed-arrays@1.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
@@ -4108,6 +4127,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 4.0.1
|
flat-cache: 4.0.1
|
||||||
|
|
||||||
|
file-selector@2.1.2:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
file-uri-to-path@1.0.0:
|
file-uri-to-path@1.0.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -4740,6 +4763,13 @@ snapshots:
|
|||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
scheduler: 0.27.0
|
scheduler: 0.27.0
|
||||||
|
|
||||||
|
react-dropzone@14.3.8(react@19.2.0):
|
||||||
|
dependencies:
|
||||||
|
attr-accept: 2.2.5
|
||||||
|
file-selector: 2.1.2
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 19.2.0
|
||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
react@19.2.0: {}
|
react@19.2.0: {}
|
||||||
|
|||||||
78
src/app/UploadFiles.tsx
Normal file
78
src/app/UploadFiles.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import { useDropzone } from "react-dropzone";
|
||||||
|
|
||||||
|
type ExtendedFile = File & {
|
||||||
|
/** can sometimes be an empty string */
|
||||||
|
webkitRelativePath?: string;
|
||||||
|
path?: string;
|
||||||
|
relativePath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreatedFile = {
|
||||||
|
id: number;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UploadFiles() {
|
||||||
|
const [createdFiles, setCreatedFiles] = useState<CreatedFile[]>([]);
|
||||||
|
|
||||||
|
const onDrop = useCallback(async (acceptedFiles: ExtendedFile[]) => {
|
||||||
|
if (acceptedFiles.length === 0) return;
|
||||||
|
|
||||||
|
const newCreatedFiles: CreatedFile[] = [];
|
||||||
|
for (const file of acceptedFiles) {
|
||||||
|
const path =
|
||||||
|
file.path || file.relativePath || file.webkitRelativePath || file.name;
|
||||||
|
const normalizedPath = path.replace(/^\.?\//, ""); // removes leading ./ or /
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
formData.append("path", normalizedPath);
|
||||||
|
|
||||||
|
const res = await fetch("/api/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
newCreatedFiles.push(data.createdFile);
|
||||||
|
}
|
||||||
|
setCreatedFiles(newCreatedFiles);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold mb-4">Upload Files</h3>
|
||||||
|
<div
|
||||||
|
{...getRootProps()}
|
||||||
|
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
|
||||||
|
isDragActive
|
||||||
|
? "border-blue-500 bg-blue-50"
|
||||||
|
: "border-gray-300 hover:border-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
{isDragActive ? (
|
||||||
|
<p>Drop the files here...</p>
|
||||||
|
) : (
|
||||||
|
<p>Drag & drop files here, or click to select</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ul className="list-disc list-inside mt-4">
|
||||||
|
{createdFiles.map((file) => (
|
||||||
|
<li key={file.id}>
|
||||||
|
Created file #{file.id}: {file.path} ({file.size} bytes)
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/app/api/upload/route.ts
Normal file
48
src/app/api/upload/route.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { db, files, users } from "@/db";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const USER_ID = 1;
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
await assertUserExists();
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
const formFile = formData.get("file") as File | null;
|
||||||
|
const path = formData.get("path") as string;
|
||||||
|
|
||||||
|
if (!formFile) {
|
||||||
|
return NextResponse.json({ message: "No file uploaded" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(users)
|
||||||
|
.values({
|
||||||
|
id: 1,
|
||||||
|
email: "test@test.com",
|
||||||
|
name: "Test User",
|
||||||
|
})
|
||||||
|
.onConflictDoNothing();
|
||||||
|
|
||||||
|
const [createdFile] = await db
|
||||||
|
.insert(files)
|
||||||
|
.values({
|
||||||
|
ownerId: 1,
|
||||||
|
path,
|
||||||
|
size: formFile.size,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
createdFile,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertUserExists = () =>
|
||||||
|
db
|
||||||
|
.insert(users)
|
||||||
|
.values({
|
||||||
|
id: USER_ID,
|
||||||
|
email: "test@test.com",
|
||||||
|
name: "Test User",
|
||||||
|
})
|
||||||
|
.onConflictDoNothing();
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import { UploadFiles } from "./UploadFiles";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return <div><h1>Files</h1></div>;
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-10">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Enlaye Files</h1>
|
||||||
|
<UploadFiles />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { sqliteTable, text, integer, foreignKey } from "drizzle-orm/sqlite-core";
|
import {
|
||||||
|
foreignKey,
|
||||||
|
integer,
|
||||||
|
sqliteTable,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
export const users = sqliteTable("users", {
|
export const users = sqliteTable("users", {
|
||||||
id: integer().primaryKey({ autoIncrement: true }),
|
id: integer().primaryKey({ autoIncrement: true }),
|
||||||
@@ -6,13 +11,18 @@ export const users = sqliteTable("users", {
|
|||||||
email: text().notNull().unique(),
|
email: text().notNull().unique(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const files = sqliteTable("files", {
|
export const files = sqliteTable(
|
||||||
id: integer().primaryKey({ autoIncrement: true }),
|
"files",
|
||||||
ownerId: integer().notNull(),
|
{
|
||||||
path: text().notNull(),
|
id: integer().primaryKey({ autoIncrement: true }),
|
||||||
}, (table) => [
|
ownerId: integer("owner_id").notNull(),
|
||||||
foreignKey({
|
path: text().notNull(),
|
||||||
columns: [table.ownerId],
|
size: integer().notNull(),
|
||||||
foreignColumns: [users.id],
|
},
|
||||||
}),
|
(table) => [
|
||||||
]);
|
foreignKey({
|
||||||
|
columns: [table.ownerId],
|
||||||
|
foreignColumns: [users.id],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user