Files UI complete with search, sharing via symlink.
This commit is contained in:
15
README.md
15
README.md
@@ -60,20 +60,23 @@ Just push to the `main` branch.
|
|||||||
|
|
||||||
- [x] Dashboard with URLs and Notes
|
- [x] Dashboard with URLs and Notes
|
||||||
- [x] News
|
- [x] News
|
||||||
- [ ] Files UI
|
- [x] Files UI
|
||||||
- [ ] Notes UI
|
|
||||||
- [ ] Photos UI
|
|
||||||
- [ ] Desktop app for selective file sync (WebDav or potentially just `rclone` or `rsync`)
|
- [ ] Desktop app for selective file sync (WebDav or potentially just `rclone` or `rsync`)
|
||||||
- [ ] Mobile app for offline file sync
|
- [ ] Mobile app for offline file sync
|
||||||
- [ ] Add notes support for mobile app
|
- [ ] Add notes support for mobile app
|
||||||
- [ ] Add photos/sync support for mobile client
|
- [ ] Add photos/sync support for mobile client
|
||||||
|
- [ ] Notes UI
|
||||||
|
- [ ] Photos UI
|
||||||
- [ ] Address `TODO:`s in code
|
- [ ] Address `TODO:`s in code
|
||||||
- [ ] Basic Contacts UI via CardDav?
|
|
||||||
- [ ] Basic Calendar UI via CalDav?
|
|
||||||
- [ ] Basic Tasks UI via CalDav?
|
|
||||||
|
|
||||||
## Where's Contacts/Calendar (CardDav/CalDav)?! Wasn't this supposed to be a core Nextcloud replacement?
|
## Where's Contacts/Calendar (CardDav/CalDav)?! Wasn't this supposed to be a core Nextcloud replacement?
|
||||||
|
|
||||||
[Check this tag/release for more info and the code where/when that was being done](https://github.com/bewcloud/bewcloud/releases/tag/v0.0.1-self-made-carddav-caldav). Contacts/CardDav worked and Calendar/CalDav mostly worked as well at that point.
|
[Check this tag/release for more info and the code where/when that was being done](https://github.com/bewcloud/bewcloud/releases/tag/v0.0.1-self-made-carddav-caldav). Contacts/CardDav worked and Calendar/CalDav mostly worked as well at that point.
|
||||||
|
|
||||||
My focus is still to get me to replace Nextcloud for me and my family ASAP, but turns out it's not easy to do it all in a single, installable _thing_, so I'm focusing on the Files UI, sync, and sharing, since [Radicale](https://radicale.org/v3.html) solved my other issues better than my own solution (and it's already _very_ efficient).
|
My focus is still to get me to replace Nextcloud for me and my family ASAP, but turns out it's not easy to do it all in a single, installable _thing_, so I'm focusing on the Files UI, sync, and sharing, since [Radicale](https://radicale.org/v3.html) solved my other issues better than my own solution (and it's already _very_ efficient).
|
||||||
|
|
||||||
|
## How does file sharing work?
|
||||||
|
|
||||||
|
[Check this PR for advanced sharing with internal and external users, with read and write access that was being done and almost working](https://github.com/bewcloud/bewcloud/pull/4). I ditched all that complexity for simply using [symlinks](https://en.wikipedia.org/wiki/Symbolic_link), as it served my use case (I have multiple data backups and trust the people I provide accounts to, with the symlinks).
|
||||||
|
|
||||||
|
You can simply `ln -s /<absolute-path-to-data-files>/<owner-user-id>/<directory-to-share> /<absolute-path-to-data-files>/<user-id-to-share-with>/` to create a shared directory between two users, and the same directory can have different names, now.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useSignal } from '@preact/signals';
|
|||||||
import { useEffect } from 'preact/hooks';
|
import { useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
import { Directory, DirectoryFile } from '/lib/types.ts';
|
import { Directory, DirectoryFile } from '/lib/types.ts';
|
||||||
// import { RequestBody, ResponseBody } from '/routes/api/files/search.tsx';
|
import { RequestBody, ResponseBody } from '/routes/api/files/search.tsx';
|
||||||
interface SearchFilesProps {}
|
interface SearchFilesProps {}
|
||||||
|
|
||||||
export default function SearchFiles({}: SearchFilesProps) {
|
export default function SearchFiles({}: SearchFilesProps) {
|
||||||
@@ -32,31 +32,30 @@ export default function SearchFiles({}: SearchFilesProps) {
|
|||||||
|
|
||||||
areResultsVisible.value = false;
|
areResultsVisible.value = false;
|
||||||
|
|
||||||
searchTimeout.value = setTimeout(() => {
|
searchTimeout.value = setTimeout(async () => {
|
||||||
isSearching.value = true;
|
isSearching.value = true;
|
||||||
|
|
||||||
// TODO: Build this
|
try {
|
||||||
// try {
|
const requestBody: RequestBody = { searchTerm };
|
||||||
// const requestBody: RequestBody = { searchTerm };
|
const response = await fetch(`/api/files/search`, {
|
||||||
// const response = await fetch(`/api/files/search`, {
|
method: 'POST',
|
||||||
// method: 'POST',
|
body: JSON.stringify(requestBody),
|
||||||
// body: JSON.stringify(requestBody),
|
});
|
||||||
// });
|
const result = await response.json() as ResponseBody;
|
||||||
// const result = await response.json() as ResponseBody;
|
|
||||||
|
|
||||||
// if (!result.success) {
|
if (!result.success) {
|
||||||
// throw new Error('Failed to search files!');
|
throw new Error('Failed to search files!');
|
||||||
// }
|
}
|
||||||
|
|
||||||
// matchingDirectories.value = result.matchingDirectories;
|
matchingDirectories.value = [...result.directories];
|
||||||
// matchingFiles.value = result.matchingFiles;
|
matchingFiles.value = [...result.files];
|
||||||
|
|
||||||
// if (matchingDirectories.value.length > 0 || matchingFiles.value.length > 0) {
|
if (matchingDirectories.value.length > 0 || matchingFiles.value.length > 0) {
|
||||||
// areResultsVisible.value = true;
|
areResultsVisible.value = true;
|
||||||
// }
|
}
|
||||||
// } catch (error) {
|
} catch (error) {
|
||||||
// console.error(error);
|
console.error(error);
|
||||||
// }
|
}
|
||||||
|
|
||||||
isSearching.value = false;
|
isSearching.value = false;
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -104,9 +103,9 @@ export default function SearchFiles({}: SearchFilesProps) {
|
|||||||
{isSearching.value ? <img src='/images/loading.svg' class='white mr-2' width={18} height={18} /> : null}
|
{isSearching.value ? <img src='/images/loading.svg' class='white mr-2' width={18} height={18} /> : null}
|
||||||
{areResultsVisible.value
|
{areResultsVisible.value
|
||||||
? (
|
? (
|
||||||
<section class='relative inline-block text-left ml-2 text-xs'>
|
<section class='relative inline-block text-left ml-2 text-sm'>
|
||||||
<section
|
<section
|
||||||
class={`absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none`}
|
class={`absolute right-0 z-10 mt-2 w-80 origin-top-right rounded-md bg-slate-600 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none overflow-y-scroll max-h-[80%] min-h-56`}
|
||||||
role='menu'
|
role='menu'
|
||||||
aria-orientation='vertical'
|
aria-orientation='vertical'
|
||||||
aria-labelledby='view-button'
|
aria-labelledby='view-button'
|
||||||
@@ -118,13 +117,13 @@ export default function SearchFiles({}: SearchFilesProps) {
|
|||||||
<li class='mb-1'>
|
<li class='mb-1'>
|
||||||
<a
|
<a
|
||||||
href={`/files?path=${directory.parent_path}${directory.directory_name}`}
|
href={`/files?path=${directory.parent_path}${directory.directory_name}`}
|
||||||
class={`block px-2 py-2 hover:no-underline hover:opacity-60`}
|
class={`block px-2 py-2 hover:no-underline hover:opacity-60 bg-slate-700 cursor-pointer font-normal`}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
<time
|
<time
|
||||||
datetime={new Date(directory.updated_at).toISOString()}
|
datetime={new Date(directory.updated_at).toISOString()}
|
||||||
class='mr-2 flex-none text-slate-100 block'
|
class='mr-2 flex-none text-slate-100 block text-xs'
|
||||||
>
|
>
|
||||||
{dateFormat.format(new Date(directory.updated_at))}
|
{dateFormat.format(new Date(directory.updated_at))}
|
||||||
</time>
|
</time>
|
||||||
@@ -138,13 +137,13 @@ export default function SearchFiles({}: SearchFilesProps) {
|
|||||||
<li class='mb-1'>
|
<li class='mb-1'>
|
||||||
<a
|
<a
|
||||||
href={`/files/open/${file.file_name}?path=${file.parent_path}`}
|
href={`/files/open/${file.file_name}?path=${file.parent_path}`}
|
||||||
class={`block px-2 py-2 hover:no-underline hover:opacity-60`}
|
class={`block px-2 py-2 hover:no-underline hover:opacity-60 bg-slate-700 cursor-pointer font-normal`}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
<time
|
<time
|
||||||
datetime={new Date(file.updated_at).toISOString()}
|
datetime={new Date(file.updated_at).toISOString()}
|
||||||
class='mr-2 flex-none text-slate-100 block'
|
class='mr-2 flex-none text-slate-100 block text-xs'
|
||||||
>
|
>
|
||||||
{dateFormat.format(new Date(file.updated_at))}
|
{dateFormat.format(new Date(file.updated_at))}
|
||||||
</time>
|
</time>
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
SET statement_timeout = 0;
|
|
||||||
SET lock_timeout = 0;
|
|
||||||
SET idle_in_transaction_session_timeout = 0;
|
|
||||||
SET client_encoding = 'UTF8';
|
|
||||||
SET standard_conforming_strings = on;
|
|
||||||
SELECT pg_catalog.set_config('search_path', '', false);
|
|
||||||
SET check_function_bodies = false;
|
|
||||||
SET xmloption = content;
|
|
||||||
SET client_min_messages = warning;
|
|
||||||
SET row_security = off;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: bewcloud_file_shares; Type: TABLE; Schema: public; Owner: postgres
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.bewcloud_file_shares (
|
|
||||||
id uuid DEFAULT gen_random_uuid(),
|
|
||||||
owner_user_id uuid DEFAULT gen_random_uuid(),
|
|
||||||
owner_parent_path text NOT NULL,
|
|
||||||
parent_path text NOT NULL,
|
|
||||||
name text NOT NULL,
|
|
||||||
type varchar NOT NULL,
|
|
||||||
user_ids_with_read_access uuid[] NOT NULL,
|
|
||||||
user_ids_with_write_access uuid[] NOT NULL,
|
|
||||||
extra jsonb NOT NULL,
|
|
||||||
created_at timestamp with time zone DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.bewcloud_file_shares OWNER TO postgres;
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX bewcloud_file_shares_unique_index ON public.bewcloud_file_shares ( owner_user_id, owner_parent_path, name, type );
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: bewcloud_file_shares bewcloud_file_shares_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.bewcloud_file_shares
|
|
||||||
ADD CONSTRAINT bewcloud_file_shares_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: bewcloud_file_shares bewcloud_file_shares_owner_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.bewcloud_file_shares
|
|
||||||
ADD CONSTRAINT bewcloud_file_shares_owner_user_id_fkey FOREIGN KEY (owner_user_id) REFERENCES public.bewcloud_users(id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: TABLE bewcloud_file_shares; Type: ACL; Schema: public; Owner: postgres
|
|
||||||
--
|
|
||||||
|
|
||||||
GRANT ALL ON TABLE public.bewcloud_file_shares TO postgres;
|
|
||||||
@@ -15,6 +15,7 @@ import * as $api_files_move_directory from './routes/api/files/move-directory.ts
|
|||||||
import * as $api_files_move from './routes/api/files/move.tsx';
|
import * as $api_files_move from './routes/api/files/move.tsx';
|
||||||
import * as $api_files_rename_directory from './routes/api/files/rename-directory.tsx';
|
import * as $api_files_rename_directory from './routes/api/files/rename-directory.tsx';
|
||||||
import * as $api_files_rename from './routes/api/files/rename.tsx';
|
import * as $api_files_rename from './routes/api/files/rename.tsx';
|
||||||
|
import * as $api_files_search from './routes/api/files/search.tsx';
|
||||||
import * as $api_files_upload from './routes/api/files/upload.tsx';
|
import * as $api_files_upload from './routes/api/files/upload.tsx';
|
||||||
import * as $api_news_add_feed from './routes/api/news/add-feed.tsx';
|
import * as $api_news_add_feed from './routes/api/news/add-feed.tsx';
|
||||||
import * as $api_news_delete_feed from './routes/api/news/delete-feed.tsx';
|
import * as $api_news_delete_feed from './routes/api/news/delete-feed.tsx';
|
||||||
@@ -54,6 +55,7 @@ const manifest = {
|
|||||||
'./routes/api/files/move.tsx': $api_files_move,
|
'./routes/api/files/move.tsx': $api_files_move,
|
||||||
'./routes/api/files/rename-directory.tsx': $api_files_rename_directory,
|
'./routes/api/files/rename-directory.tsx': $api_files_rename_directory,
|
||||||
'./routes/api/files/rename.tsx': $api_files_rename,
|
'./routes/api/files/rename.tsx': $api_files_rename,
|
||||||
|
'./routes/api/files/search.tsx': $api_files_search,
|
||||||
'./routes/api/files/upload.tsx': $api_files_upload,
|
'./routes/api/files/upload.tsx': $api_files_upload,
|
||||||
'./routes/api/news/add-feed.tsx': $api_news_add_feed,
|
'./routes/api/news/add-feed.tsx': $api_news_add_feed,
|
||||||
'./routes/api/news/delete-feed.tsx': $api_news_delete_feed,
|
'./routes/api/news/delete-feed.tsx': $api_news_delete_feed,
|
||||||
|
|||||||
@@ -1,66 +1,24 @@
|
|||||||
import { join } from 'std/path/join.ts';
|
import { join } from 'std/path/join.ts';
|
||||||
|
|
||||||
import Database, { sql } from '/lib/interfaces/database.ts';
|
|
||||||
import { getFilesRootPath } from '/lib/config.ts';
|
import { getFilesRootPath } from '/lib/config.ts';
|
||||||
import { Directory, DirectoryFile, FileShare, FileShareLink } from '/lib/types.ts';
|
import { Directory, DirectoryFile } from '/lib/types.ts';
|
||||||
import { sortDirectoriesByName, sortEntriesByName, sortFilesByName, TRASH_PATH } from '/lib/utils/files.ts';
|
import { sortDirectoriesByName, sortEntriesByName, sortFilesByName, TRASH_PATH } from '/lib/utils/files.ts';
|
||||||
|
|
||||||
const db = new Database();
|
|
||||||
|
|
||||||
export async function getDirectories(userId: string, path: string): Promise<Directory[]> {
|
export async function getDirectories(userId: string, path: string): Promise<Directory[]> {
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
const directoryShares = await db.query<FileShare>(
|
|
||||||
sql`SELECT * FROM "bewcloud_file_shares"
|
|
||||||
WHERE "parent_path" = $2
|
|
||||||
AND "type" = 'directory'
|
|
||||||
AND (
|
|
||||||
"owner_user_id" = $1
|
|
||||||
OR ANY("user_ids_with_read_access") = $1
|
|
||||||
OR ANY("user_ids_with_write_access") = $1
|
|
||||||
)`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
path,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const directories: Directory[] = [];
|
const directories: Directory[] = [];
|
||||||
|
|
||||||
const directoryEntries = (await getPathEntries(userId, path)).filter((entry) => entry.isDirectory);
|
const directoryEntries = (await getPathEntries(userId, path)).filter((entry) => entry.isDirectory || entry.isSymlink);
|
||||||
|
|
||||||
for (const entry of directoryEntries) {
|
for (const entry of directoryEntries) {
|
||||||
const stat = await Deno.stat(join(rootPath, entry.name));
|
const stat = await Deno.stat(join(rootPath, entry.name));
|
||||||
|
|
||||||
const directory: Directory = {
|
const directory: Directory = {
|
||||||
owner_user_id: userId,
|
user_id: userId,
|
||||||
parent_path: path,
|
parent_path: path,
|
||||||
directory_name: entry.name,
|
directory_name: entry.name,
|
||||||
has_write_access: true,
|
has_write_access: true,
|
||||||
file_share: directoryShares.find((share) =>
|
|
||||||
share.owner_user_id === userId && share.parent_path === path && share.name === entry.name
|
|
||||||
),
|
|
||||||
size_in_bytes: stat.size,
|
|
||||||
updated_at: stat.mtime || new Date(),
|
|
||||||
created_at: stat.birthtime || new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
directories.push(directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add directoryShares that aren't owned by this user
|
|
||||||
const foreignDirectoryShares = directoryShares.filter((directoryShare) => directoryShare.owner_user_id !== userId);
|
|
||||||
for (const share of foreignDirectoryShares) {
|
|
||||||
const stat = await Deno.stat(join(getFilesRootPath(), share.owner_user_id, path, share.name));
|
|
||||||
|
|
||||||
const hasWriteAccess = share.user_ids_with_write_access.includes(userId);
|
|
||||||
|
|
||||||
const directory: Directory = {
|
|
||||||
owner_user_id: share.owner_user_id,
|
|
||||||
parent_path: path,
|
|
||||||
directory_name: share.name,
|
|
||||||
has_write_access: hasWriteAccess,
|
|
||||||
file_share: share,
|
|
||||||
size_in_bytes: stat.size,
|
size_in_bytes: stat.size,
|
||||||
updated_at: stat.mtime || new Date(),
|
updated_at: stat.mtime || new Date(),
|
||||||
created_at: stat.birthtime || new Date(),
|
created_at: stat.birthtime || new Date(),
|
||||||
@@ -77,21 +35,6 @@ export async function getDirectories(userId: string, path: string): Promise<Dire
|
|||||||
export async function getFiles(userId: string, path: string): Promise<DirectoryFile[]> {
|
export async function getFiles(userId: string, path: string): Promise<DirectoryFile[]> {
|
||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
const fileShares = await db.query<FileShare>(
|
|
||||||
sql`SELECT * FROM "bewcloud_file_shares"
|
|
||||||
WHERE "parent_path" = $2
|
|
||||||
AND "type" = 'file'
|
|
||||||
AND (
|
|
||||||
"owner_user_id" = $1
|
|
||||||
OR ANY("user_ids_with_read_access") = $1
|
|
||||||
OR ANY("user_ids_with_write_access") = $1
|
|
||||||
)`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
path,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const files: DirectoryFile[] = [];
|
const files: DirectoryFile[] = [];
|
||||||
|
|
||||||
const fileEntries = (await getPathEntries(userId, path)).filter((entry) => entry.isFile);
|
const fileEntries = (await getPathEntries(userId, path)).filter((entry) => entry.isFile);
|
||||||
@@ -100,13 +43,10 @@ export async function getFiles(userId: string, path: string): Promise<DirectoryF
|
|||||||
const stat = await Deno.stat(join(rootPath, entry.name));
|
const stat = await Deno.stat(join(rootPath, entry.name));
|
||||||
|
|
||||||
const file: DirectoryFile = {
|
const file: DirectoryFile = {
|
||||||
owner_user_id: userId,
|
user_id: userId,
|
||||||
parent_path: path,
|
parent_path: path,
|
||||||
file_name: entry.name,
|
file_name: entry.name,
|
||||||
has_write_access: true,
|
has_write_access: true,
|
||||||
file_share: fileShares.find((share) =>
|
|
||||||
share.owner_user_id === userId && share.parent_path === path && share.name === entry.name
|
|
||||||
),
|
|
||||||
size_in_bytes: stat.size,
|
size_in_bytes: stat.size,
|
||||||
updated_at: stat.mtime || new Date(),
|
updated_at: stat.mtime || new Date(),
|
||||||
created_at: stat.birthtime || new Date(),
|
created_at: stat.birthtime || new Date(),
|
||||||
@@ -115,29 +55,6 @@ export async function getFiles(userId: string, path: string): Promise<DirectoryF
|
|||||||
files.push(file);
|
files.push(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add fileShares that aren't owned by this user
|
|
||||||
const foreignFileShares = fileShares.filter((fileShare) => fileShare.owner_user_id !== userId);
|
|
||||||
for (const share of foreignFileShares) {
|
|
||||||
const stat = await Deno.stat(join(getFilesRootPath(), share.owner_user_id, path, share.name));
|
|
||||||
|
|
||||||
const hasWriteAccess = share.user_ids_with_write_access.includes(userId);
|
|
||||||
|
|
||||||
const file: DirectoryFile = {
|
|
||||||
owner_user_id: share.owner_user_id,
|
|
||||||
parent_path: path,
|
|
||||||
file_name: share.name,
|
|
||||||
has_write_access: hasWriteAccess,
|
|
||||||
file_share: share,
|
|
||||||
size_in_bytes: stat.size,
|
|
||||||
updated_at: stat.mtime || new Date(),
|
|
||||||
created_at: stat.birthtime || new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
files.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Check fileshare directories and list files from there too
|
|
||||||
|
|
||||||
files.sort(sortFilesByName);
|
files.sort(sortFilesByName);
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
@@ -193,23 +110,6 @@ export async function renameDirectoryOrFile(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await Deno.rename(join(oldRootPath, oldName), join(newRootPath, newName));
|
await Deno.rename(join(oldRootPath, oldName), join(newRootPath, newName));
|
||||||
|
|
||||||
// Update any matching file shares
|
|
||||||
await db.query<FileShare>(
|
|
||||||
sql`UPDATE "bewcloud_file_shares" SET
|
|
||||||
"parent_path" = $4,
|
|
||||||
"name" = $5
|
|
||||||
WHERE "parent_path" = $2
|
|
||||||
AND "name" = $3
|
|
||||||
AND "owner_user_id" = $1`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
oldPath,
|
|
||||||
oldName,
|
|
||||||
newPath,
|
|
||||||
newName,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return false;
|
return false;
|
||||||
@@ -227,19 +127,6 @@ export async function deleteDirectoryOrFile(userId: string, path: string, name:
|
|||||||
} else {
|
} else {
|
||||||
const trashPath = join(getFilesRootPath(), userId, TRASH_PATH);
|
const trashPath = join(getFilesRootPath(), userId, TRASH_PATH);
|
||||||
await Deno.rename(join(rootPath, name), join(trashPath, name));
|
await Deno.rename(join(rootPath, name), join(trashPath, name));
|
||||||
|
|
||||||
// Delete any matching file shares
|
|
||||||
await db.query<FileShare>(
|
|
||||||
sql`DELETE FROM "bewcloud_file_shares"
|
|
||||||
WHERE "parent_path" = $2
|
|
||||||
AND "name" = $3
|
|
||||||
AND "owner_user_id" = $1`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
path,
|
|
||||||
name,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -290,6 +177,8 @@ export async function getFile(
|
|||||||
contentType = 'image/jpeg';
|
contentType = 'image/jpeg';
|
||||||
} else if (extension === 'png') {
|
} else if (extension === 'png') {
|
||||||
contentType = 'image/png';
|
contentType = 'image/png';
|
||||||
|
} else if (extension === 'svg') {
|
||||||
|
contentType = 'image/svg+xml';
|
||||||
} else if (extension === 'pdf') {
|
} else if (extension === 'pdf') {
|
||||||
contentType = 'application/pdf';
|
contentType = 'application/pdf';
|
||||||
} else if (extension === 'txt' || extension === 'md') {
|
} else if (extension === 'txt' || extension === 'md') {
|
||||||
@@ -309,219 +198,236 @@ export async function getFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createFileShare(
|
export async function searchFilesAndDirectories(
|
||||||
userId: string,
|
userId: string,
|
||||||
path: string,
|
searchTerm: string,
|
||||||
name: string,
|
): Promise<{ success: boolean; directories: Directory[]; files: DirectoryFile[] }> {
|
||||||
type: 'directory' | 'file',
|
const directoryNamesResult = await searchDirectoryNames(userId, searchTerm);
|
||||||
userIdsWithReadAccess: string[],
|
const fileNamesResult = await searchFileNames(userId, searchTerm);
|
||||||
userIdsWithWriteAccess: string[],
|
const fileContentsResult = await searchFileContents(userId, searchTerm);
|
||||||
readShareLinks: FileShareLink[],
|
|
||||||
writeShareLinks: FileShareLink[],
|
const success = directoryNamesResult.success && fileNamesResult.success && fileContentsResult.success;
|
||||||
): Promise<FileShare> {
|
|
||||||
const extra: FileShare['extra'] = {
|
const directories = [...directoryNamesResult.directories];
|
||||||
read_share_links: readShareLinks,
|
directories.sort(sortDirectoriesByName);
|
||||||
write_share_links: writeShareLinks,
|
|
||||||
|
const files = [...fileNamesResult.files, ...fileContentsResult.files];
|
||||||
|
files.sort(sortFilesByName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
directories,
|
||||||
|
files,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newFileShare = (await db.query<FileShare>(
|
|
||||||
sql`INSERT INTO "bewcloud_file_shares" (
|
|
||||||
"owner_user_id",
|
|
||||||
"owner_parent_path",
|
|
||||||
"parent_path",
|
|
||||||
"name",
|
|
||||||
"type",
|
|
||||||
"user_ids_with_read_access",
|
|
||||||
"user_ids_with_write_access",
|
|
||||||
"extra"
|
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
||||||
RETURNING *`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
path,
|
|
||||||
'/',
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
userIdsWithReadAccess,
|
|
||||||
userIdsWithWriteAccess,
|
|
||||||
JSON.stringify(extra),
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
return newFileShare;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateFileShare(fileShare: FileShare) {
|
async function searchDirectoryNames(
|
||||||
await db.query(
|
|
||||||
sql`UPDATE "bewcloud_file_shares" SET
|
|
||||||
"owner_parent_path" = $2,
|
|
||||||
"parent_path" = $3,
|
|
||||||
"name" = $4,
|
|
||||||
"user_ids_with_read_access" = $5,
|
|
||||||
"user_ids_with_write_access" = $6,
|
|
||||||
"extra" = $7
|
|
||||||
WHERE "id" = $1`,
|
|
||||||
[
|
|
||||||
fileShare.id,
|
|
||||||
fileShare.owner_parent_path,
|
|
||||||
fileShare.parent_path,
|
|
||||||
fileShare.name,
|
|
||||||
fileShare.user_ids_with_read_access,
|
|
||||||
fileShare.user_ids_with_write_access,
|
|
||||||
JSON.stringify(fileShare.extra),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDirectoryAccess(
|
|
||||||
userId: string,
|
userId: string,
|
||||||
parentPath: string,
|
searchTerm: string,
|
||||||
name?: string,
|
): Promise<{ success: boolean; directories: Directory[] }> {
|
||||||
): Promise<{ hasReadAccess: boolean; hasWriteAccess: boolean; ownerUserId: string; ownerParentPath: string }> {
|
const rootPath = `${getFilesRootPath()}/${userId}/`;
|
||||||
const rootPath = join(getFilesRootPath(), userId, parentPath, name || '');
|
|
||||||
|
const directories: Directory[] = [];
|
||||||
|
|
||||||
// If it exists in the correct filesystem path, it's the user's
|
|
||||||
try {
|
try {
|
||||||
await Deno.stat(rootPath);
|
const controller = new AbortController();
|
||||||
|
const commandTimeout = setTimeout(() => controller.abort(), 10_000);
|
||||||
|
|
||||||
return { hasReadAccess: true, hasWriteAccess: true, ownerUserId: userId, ownerParentPath: parentPath };
|
const command = new Deno.Command(`find`, {
|
||||||
|
args: [
|
||||||
|
`.`, // proper cwd is sent below
|
||||||
|
`-type`,
|
||||||
|
`d,l`, // directories and symbolic links
|
||||||
|
`-iname`,
|
||||||
|
`*${searchTerm}*`,
|
||||||
|
],
|
||||||
|
cwd: rootPath,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code, stdout, stderr } = await command.output();
|
||||||
|
|
||||||
|
if (commandTimeout) {
|
||||||
|
clearTimeout(commandTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
if (stderr) {
|
||||||
|
throw new Error(new TextDecoder().decode(stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown error running "find"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = new TextDecoder().decode(stdout);
|
||||||
|
const matchingDirectories = output.split('\n').map((directoryPath) => directoryPath.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
for (const relativeDirectoryPath of matchingDirectories) {
|
||||||
|
const stat = await Deno.stat(join(rootPath, relativeDirectoryPath));
|
||||||
|
let parentPath = `/${relativeDirectoryPath.replace('./', '/').split('/').slice(0, -1).join('')}/`;
|
||||||
|
const directoryName = relativeDirectoryPath.split('/').pop()!;
|
||||||
|
|
||||||
|
if (parentPath === '//') {
|
||||||
|
parentPath = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
const directory: Directory = {
|
||||||
|
user_id: userId,
|
||||||
|
parent_path: parentPath,
|
||||||
|
directory_name: directoryName,
|
||||||
|
has_write_access: true,
|
||||||
|
size_in_bytes: stat.size,
|
||||||
|
updated_at: stat.mtime || new Date(),
|
||||||
|
created_at: stat.birthtime || new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
directories.push(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, directories };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise check if it's been shared with them
|
return { success: false, directories };
|
||||||
const parentPaths: string[] = [];
|
|
||||||
let nextParentPath: string | null = rootPath;
|
|
||||||
|
|
||||||
while (nextParentPath !== null) {
|
|
||||||
parentPaths.push(nextParentPath);
|
|
||||||
|
|
||||||
nextParentPath = `/${nextParentPath.split('/').filter(Boolean).slice(0, -1).join('/')}`;
|
|
||||||
|
|
||||||
if (nextParentPath === '/') {
|
|
||||||
parentPaths.push(nextParentPath);
|
|
||||||
nextParentPath = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileShare = (await db.query<FileShare>(
|
|
||||||
sql`SELECT * FROM "bewcloud_file_shares"
|
|
||||||
WHERE "parent_path" = ANY($2)
|
|
||||||
AND "name" = $3
|
|
||||||
AND "type" = 'directory'
|
|
||||||
AND (
|
|
||||||
ANY("user_ids_with_read_access") = $1
|
|
||||||
OR ANY("user_ids_with_write_access") = $1
|
|
||||||
)
|
|
||||||
ORDER BY "parent_path" ASC
|
|
||||||
LIMIT 1`,
|
|
||||||
[
|
|
||||||
userId,
|
|
||||||
parentPaths,
|
|
||||||
name,
|
|
||||||
],
|
|
||||||
))[0];
|
|
||||||
|
|
||||||
if (fileShare) {
|
|
||||||
return {
|
|
||||||
hasReadAccess: fileShare.user_ids_with_read_access.includes(userId) ||
|
|
||||||
fileShare.user_ids_with_write_access.includes(userId),
|
|
||||||
hasWriteAccess: fileShare.user_ids_with_write_access.includes(userId),
|
|
||||||
ownerUserId: fileShare.owner_user_id,
|
|
||||||
ownerParentPath: fileShare.owner_parent_path,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { hasReadAccess: false, hasWriteAccess: false, ownerUserId: userId, ownerParentPath: parentPath };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFileAccess(
|
async function searchFileNames(
|
||||||
userId: string,
|
userId: string,
|
||||||
parentPath: string,
|
searchTerm: string,
|
||||||
name: string,
|
): Promise<{ success: boolean; files: DirectoryFile[] }> {
|
||||||
): Promise<{ hasReadAccess: boolean; hasWriteAccess: boolean; ownerUserId: string; ownerParentPath: string }> {
|
const rootPath = `${getFilesRootPath()}/${userId}/`;
|
||||||
const rootPath = join(getFilesRootPath(), userId, parentPath, name);
|
|
||||||
|
const files: DirectoryFile[] = [];
|
||||||
|
|
||||||
// If it exists in the correct filesystem path, it's the user's
|
|
||||||
try {
|
try {
|
||||||
await Deno.stat(rootPath);
|
const controller = new AbortController();
|
||||||
|
const commandTimeout = setTimeout(() => controller.abort(), 10_000);
|
||||||
|
|
||||||
return { hasReadAccess: true, hasWriteAccess: true, ownerUserId: userId, ownerParentPath: parentPath };
|
const command = new Deno.Command(`find`, {
|
||||||
|
args: [
|
||||||
|
`.`, // proper cwd is sent below
|
||||||
|
`-type`,
|
||||||
|
`f`,
|
||||||
|
`-iname`,
|
||||||
|
`*${searchTerm}*`,
|
||||||
|
],
|
||||||
|
cwd: rootPath,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code, stdout, stderr } = await command.output();
|
||||||
|
|
||||||
|
if (commandTimeout) {
|
||||||
|
clearTimeout(commandTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
if (stderr) {
|
||||||
|
throw new Error(new TextDecoder().decode(stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown error running "find"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = new TextDecoder().decode(stdout);
|
||||||
|
const matchingFiles = output.split('\n').map((filePath) => filePath.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
for (const relativeFilePath of matchingFiles) {
|
||||||
|
const stat = await Deno.stat(join(rootPath, relativeFilePath));
|
||||||
|
let parentPath = `/${relativeFilePath.replace('./', '/').split('/').slice(0, -1).join('')}/`;
|
||||||
|
const fileName = relativeFilePath.split('/').pop()!;
|
||||||
|
|
||||||
|
if (parentPath === '//') {
|
||||||
|
parentPath = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
const file: DirectoryFile = {
|
||||||
|
user_id: userId,
|
||||||
|
parent_path: parentPath,
|
||||||
|
file_name: fileName,
|
||||||
|
has_write_access: true,
|
||||||
|
size_in_bytes: stat.size,
|
||||||
|
updated_at: stat.mtime || new Date(),
|
||||||
|
created_at: stat.birthtime || new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, files };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise check if it's been shared with them
|
return { success: false, files };
|
||||||
let fileShare = (await db.query<FileShare>(
|
}
|
||||||
sql`SELECT * FROM "bewcloud_file_shares"
|
|
||||||
WHERE "parent_path" = $2
|
async function searchFileContents(
|
||||||
AND "name" = $3
|
userId: string,
|
||||||
AND "type" = 'file'
|
searchTerm: string,
|
||||||
AND (
|
): Promise<{ success: boolean; files: DirectoryFile[] }> {
|
||||||
ANY("user_ids_with_read_access") = $1
|
const rootPath = `${getFilesRootPath()}/${userId}/`;
|
||||||
OR ANY("user_ids_with_write_access") = $1
|
|
||||||
)
|
const files: DirectoryFile[] = [];
|
||||||
ORDER BY "parent_path" ASC
|
|
||||||
LIMIT 1`,
|
try {
|
||||||
[
|
const controller = new AbortController();
|
||||||
userId,
|
const commandTimeout = setTimeout(() => controller.abort(), 10_000);
|
||||||
parentPath,
|
|
||||||
name,
|
const command = new Deno.Command(`grep`, {
|
||||||
],
|
args: [
|
||||||
))[0];
|
`-rHisl`,
|
||||||
|
`${searchTerm}`,
|
||||||
if (fileShare) {
|
`.`, // proper cwd is sent below
|
||||||
return {
|
],
|
||||||
hasReadAccess: fileShare.user_ids_with_read_access.includes(userId) ||
|
cwd: rootPath,
|
||||||
fileShare.user_ids_with_write_access.includes(userId),
|
signal: controller.signal,
|
||||||
hasWriteAccess: fileShare.user_ids_with_write_access.includes(userId),
|
});
|
||||||
ownerUserId: fileShare.owner_user_id,
|
|
||||||
ownerParentPath: fileShare.owner_parent_path,
|
const { code, stdout, stderr } = await command.output();
|
||||||
};
|
|
||||||
}
|
if (commandTimeout) {
|
||||||
|
clearTimeout(commandTimeout);
|
||||||
// Otherwise check if it's a parent directory has been shared with them, which would also give them access
|
}
|
||||||
const parentPaths: string[] = [];
|
|
||||||
let nextParentPath: string | null = rootPath;
|
if (code !== 0) {
|
||||||
|
if (stderr) {
|
||||||
while (nextParentPath !== null) {
|
throw new Error(new TextDecoder().decode(stderr));
|
||||||
parentPaths.push(nextParentPath);
|
}
|
||||||
|
|
||||||
nextParentPath = `/${nextParentPath.split('/').filter(Boolean).slice(0, -1).join('/')}`;
|
throw new Error(`Unknown error running "grep"`);
|
||||||
|
}
|
||||||
if (nextParentPath === '/') {
|
|
||||||
parentPaths.push(nextParentPath);
|
const output = new TextDecoder().decode(stdout);
|
||||||
nextParentPath = null;
|
const matchingFiles = output.split('\n').map((filePath) => filePath.trim()).filter(Boolean);
|
||||||
}
|
|
||||||
}
|
for (const relativeFilePath of matchingFiles) {
|
||||||
|
const stat = await Deno.stat(join(rootPath, relativeFilePath));
|
||||||
fileShare = (await db.query<FileShare>(
|
let parentPath = `/${relativeFilePath.replace('./', '/').split('/').slice(0, -1).join('')}/`;
|
||||||
sql`SELECT * FROM "bewcloud_file_shares"
|
const fileName = relativeFilePath.split('/').pop()!;
|
||||||
WHERE "parent_path" = ANY($2)
|
|
||||||
AND "name" = $3
|
if (parentPath === '//') {
|
||||||
AND "type" = 'directory'
|
parentPath = '/';
|
||||||
AND (
|
}
|
||||||
ANY("user_ids_with_read_access") = $1
|
|
||||||
OR ANY("user_ids_with_write_access") = $1
|
const file: DirectoryFile = {
|
||||||
)
|
user_id: userId,
|
||||||
ORDER BY "parent_path" ASC
|
parent_path: parentPath,
|
||||||
LIMIT 1`,
|
file_name: fileName,
|
||||||
[
|
has_write_access: true,
|
||||||
userId,
|
size_in_bytes: stat.size,
|
||||||
parentPaths,
|
updated_at: stat.mtime || new Date(),
|
||||||
name,
|
created_at: stat.birthtime || new Date(),
|
||||||
],
|
};
|
||||||
))[0];
|
|
||||||
|
files.push(file);
|
||||||
if (fileShare) {
|
}
|
||||||
return {
|
|
||||||
hasReadAccess: fileShare.user_ids_with_read_access.includes(userId) ||
|
return { success: true, files };
|
||||||
fileShare.user_ids_with_write_access.includes(userId),
|
} catch (error) {
|
||||||
hasWriteAccess: fileShare.user_ids_with_write_access.includes(userId),
|
console.error(error);
|
||||||
ownerUserId: fileShare.owner_user_id,
|
}
|
||||||
ownerParentPath: fileShare.owner_parent_path,
|
|
||||||
};
|
return { success: false, files };
|
||||||
}
|
|
||||||
|
|
||||||
return { hasReadAccess: false, hasWriteAccess: false, ownerUserId: userId, ownerParentPath: parentPath };
|
|
||||||
}
|
}
|
||||||
|
|||||||
27
lib/types.ts
27
lib/types.ts
@@ -85,44 +85,21 @@ export interface NewsFeedArticle {
|
|||||||
created_at: Date;
|
created_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileShareLink {
|
|
||||||
url: string;
|
|
||||||
hashed_password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileShare {
|
|
||||||
id: string;
|
|
||||||
owner_user_id: string;
|
|
||||||
owner_parent_path: string;
|
|
||||||
parent_path: string;
|
|
||||||
name: string;
|
|
||||||
type: 'directory' | 'file';
|
|
||||||
user_ids_with_read_access: string[];
|
|
||||||
user_ids_with_write_access: string[];
|
|
||||||
extra: {
|
|
||||||
read_share_links: FileShareLink[];
|
|
||||||
write_share_links: FileShareLink[];
|
|
||||||
};
|
|
||||||
created_at: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Directory {
|
export interface Directory {
|
||||||
owner_user_id: string;
|
user_id: string;
|
||||||
parent_path: string;
|
parent_path: string;
|
||||||
directory_name: string;
|
directory_name: string;
|
||||||
has_write_access: boolean;
|
has_write_access: boolean;
|
||||||
file_share?: FileShare;
|
|
||||||
size_in_bytes: number;
|
size_in_bytes: number;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DirectoryFile {
|
export interface DirectoryFile {
|
||||||
owner_user_id: string;
|
user_id: string;
|
||||||
parent_path: string;
|
parent_path: string;
|
||||||
file_name: string;
|
file_name: string;
|
||||||
has_write_access: boolean;
|
has_write_access: boolean;
|
||||||
file_share?: FileShare;
|
|
||||||
size_in_bytes: number;
|
size_in_bytes: number;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||||
import { createDirectory, getDirectories, getDirectoryAccess } from '/lib/data/files.ts';
|
import { createDirectory, getDirectories } from '/lib/data/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -30,22 +30,12 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hasWriteAccess, ownerUserId, ownerParentPath } = await getDirectoryAccess(
|
const createdDirectory = await createDirectory(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
requestBody.name.trim(),
|
requestBody.name.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasWriteAccess) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdDirectory = await createDirectory(
|
|
||||||
ownerUserId,
|
|
||||||
ownerParentPath,
|
|
||||||
requestBody.name.trim(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const newDirectories = await getDirectories(context.state.user.id, requestBody.parentPath);
|
const newDirectories = await getDirectories(context.state.user.id, requestBody.parentPath);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: createdDirectory, newDirectories };
|
const responseBody: ResponseBody = { success: createdDirectory, newDirectories };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||||
import { deleteDirectoryOrFile, getDirectories, getDirectoryAccess } from '/lib/data/files.ts';
|
import { deleteDirectoryOrFile, getDirectories } from '/lib/data/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -30,22 +30,12 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hasWriteAccess, ownerUserId, ownerParentPath } = await getDirectoryAccess(
|
const deletedDirectory = await deleteDirectoryOrFile(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
requestBody.name.trim(),
|
requestBody.name.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasWriteAccess) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletedDirectory = await deleteDirectoryOrFile(
|
|
||||||
ownerUserId,
|
|
||||||
ownerParentPath,
|
|
||||||
requestBody.name.trim(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const newDirectories = await getDirectories(context.state.user.id, requestBody.parentPath);
|
const newDirectories = await getDirectories(context.state.user.id, requestBody.parentPath);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: deletedDirectory, newDirectories };
|
const responseBody: ResponseBody = { success: deletedDirectory, newDirectories };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { deleteDirectoryOrFile, getDirectoryAccess, getFileAccess, getFiles } from '/lib/data/files.ts';
|
import { deleteDirectoryOrFile, getFiles } from '/lib/data/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -30,30 +30,12 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let { hasWriteAccess, ownerUserId, ownerParentPath } = await getFileAccess(
|
const deletedFile = await deleteDirectoryOrFile(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
requestBody.name.trim(),
|
requestBody.name.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasWriteAccess) {
|
|
||||||
const directoryAccessResult = await getDirectoryAccess(context.state.user.id, requestBody.parentPath);
|
|
||||||
|
|
||||||
hasWriteAccess = directoryAccessResult.hasWriteAccess;
|
|
||||||
ownerUserId = directoryAccessResult.ownerUserId;
|
|
||||||
ownerParentPath = directoryAccessResult.ownerParentPath;
|
|
||||||
|
|
||||||
if (!hasWriteAccess) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletedFile = await deleteDirectoryOrFile(
|
|
||||||
ownerUserId,
|
|
||||||
ownerParentPath,
|
|
||||||
requestBody.name.trim(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const newFiles = await getFiles(context.state.user.id, requestBody.parentPath);
|
const newFiles = await getFiles(context.state.user.id, requestBody.parentPath);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: deletedFile, newFiles };
|
const responseBody: ResponseBody = { success: deletedFile, newFiles };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||||
import { getDirectories, getDirectoryAccess, renameDirectoryOrFile } from '/lib/data/files.ts';
|
import { getDirectories, renameDirectoryOrFile } from '/lib/data/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -33,26 +33,10 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hasWriteAccess: hasOldWriteAccess, ownerUserId, ownerParentPath: oldOwnerParentPath } =
|
|
||||||
await getDirectoryAccess(context.state.user.id, requestBody.oldParentPath, requestBody.name.trim());
|
|
||||||
|
|
||||||
if (!hasOldWriteAccess) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hasWriteAccess: hasNewWriteAccess, ownerParentPath: newOwnerParentPath } = await getDirectoryAccess(
|
|
||||||
context.state.user.id,
|
|
||||||
requestBody.newParentPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasNewWriteAccess) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const movedDirectory = await renameDirectoryOrFile(
|
const movedDirectory = await renameDirectoryOrFile(
|
||||||
ownerUserId,
|
context.state.user.id,
|
||||||
oldOwnerParentPath,
|
requestBody.oldParentPath,
|
||||||
newOwnerParentPath,
|
requestBody.newParentPath,
|
||||||
requestBody.name.trim(),
|
requestBody.name.trim(),
|
||||||
requestBody.name.trim(),
|
requestBody.name.trim(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { getDirectoryAccess, getFileAccess, getFiles, renameDirectoryOrFile } from '/lib/data/files.ts';
|
import { getFiles, renameDirectoryOrFile } from '/lib/data/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -33,35 +33,10 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let { hasWriteAccess: hasOldWriteAccess, ownerUserId, ownerParentPath: oldOwnerParentPath } = await getFileAccess(
|
const movedFile = await renameDirectoryOrFile(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.oldParentPath,
|
requestBody.oldParentPath,
|
||||||
requestBody.name.trim(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasOldWriteAccess) {
|
|
||||||
const directoryAccessResult = await getDirectoryAccess(context.state.user.id, requestBody.oldParentPath);
|
|
||||||
|
|
||||||
hasOldWriteAccess = directoryAccessResult.hasWriteAccess;
|
|
||||||
ownerUserId = directoryAccessResult.ownerUserId;
|
|
||||||
oldOwnerParentPath = directoryAccessResult.ownerParentPath;
|
|
||||||
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hasWriteAccess: hasNewWriteAccess, ownerParentPath: newOwnerParentPath } = await getDirectoryAccess(
|
|
||||||
context.state.user.id,
|
|
||||||
requestBody.newParentPath,
|
requestBody.newParentPath,
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasNewWriteAccess) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const movedFile = await renameDirectoryOrFile(
|
|
||||||
ownerUserId,
|
|
||||||
oldOwnerParentPath,
|
|
||||||
newOwnerParentPath,
|
|
||||||
requestBody.name.trim(),
|
requestBody.name.trim(),
|
||||||
requestBody.name.trim(),
|
requestBody.name.trim(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { Directory, FreshContextState } from '/lib/types.ts';
|
import { Directory, FreshContextState } from '/lib/types.ts';
|
||||||
import { getDirectories, getDirectoryAccess, renameDirectoryOrFile } from '/lib/data/files.ts';
|
import { getDirectories, renameDirectoryOrFile } from '/lib/data/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -32,20 +32,10 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hasWriteAccess, ownerUserId, ownerParentPath } = await getDirectoryAccess(
|
const movedDirectory = await renameDirectoryOrFile(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
requestBody.oldName.trim(),
|
requestBody.parentPath,
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasWriteAccess) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const movedDirectory = await renameDirectoryOrFile(
|
|
||||||
ownerUserId,
|
|
||||||
ownerParentPath,
|
|
||||||
ownerParentPath,
|
|
||||||
requestBody.oldName.trim(),
|
requestBody.oldName.trim(),
|
||||||
requestBody.newName.trim(),
|
requestBody.newName.trim(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { getDirectoryAccess, getFileAccess, getFiles, renameDirectoryOrFile } from '/lib/data/files.ts';
|
import { getFiles, renameDirectoryOrFile } from '/lib/data/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -32,28 +32,10 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let { hasWriteAccess, ownerUserId, ownerParentPath } = await getFileAccess(
|
const movedFile = await renameDirectoryOrFile(
|
||||||
context.state.user.id,
|
context.state.user.id,
|
||||||
requestBody.parentPath,
|
requestBody.parentPath,
|
||||||
requestBody.oldName.trim(),
|
requestBody.parentPath,
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasWriteAccess) {
|
|
||||||
const directoryAccessResult = await getDirectoryAccess(context.state.user.id, requestBody.parentPath);
|
|
||||||
|
|
||||||
hasWriteAccess = directoryAccessResult.hasWriteAccess;
|
|
||||||
ownerUserId = directoryAccessResult.ownerUserId;
|
|
||||||
ownerParentPath = directoryAccessResult.ownerParentPath;
|
|
||||||
|
|
||||||
if (!hasWriteAccess) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const movedFile = await renameDirectoryOrFile(
|
|
||||||
ownerUserId,
|
|
||||||
ownerParentPath,
|
|
||||||
ownerParentPath,
|
|
||||||
requestBody.oldName.trim(),
|
requestBody.oldName.trim(),
|
||||||
requestBody.newName.trim(),
|
requestBody.newName.trim(),
|
||||||
);
|
);
|
||||||
|
|||||||
39
routes/api/files/search.tsx
Normal file
39
routes/api/files/search.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
|
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
|
import { searchFilesAndDirectories } from '/lib/data/files.ts';
|
||||||
|
|
||||||
|
interface Data {}
|
||||||
|
|
||||||
|
export interface RequestBody {
|
||||||
|
searchTerm: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseBody {
|
||||||
|
success: boolean;
|
||||||
|
directories: Directory[];
|
||||||
|
files: DirectoryFile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handler: Handlers<Data, FreshContextState> = {
|
||||||
|
async POST(request, context) {
|
||||||
|
if (!context.state.user) {
|
||||||
|
return new Response('Unauthorized', { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestBody = await request.clone().json() as RequestBody;
|
||||||
|
|
||||||
|
if (!requestBody.searchTerm?.trim()) {
|
||||||
|
return new Response('Bad Request', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await searchFilesAndDirectories(
|
||||||
|
context.state.user.id,
|
||||||
|
requestBody.searchTerm.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const responseBody: ResponseBody = { ...result };
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(responseBody));
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { createFile, getDirectoryAccess, getFileAccess, getFiles } from '/lib/data/files.ts';
|
import { createFile, getFiles } from '/lib/data/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -29,25 +29,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let { hasWriteAccess, ownerUserId, ownerParentPath } = await getFileAccess(
|
const createdFile = await createFile(context.state.user.id, parentPath, name.trim(), await contents.arrayBuffer());
|
||||||
context.state.user.id,
|
|
||||||
parentPath,
|
|
||||||
name.trim(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasWriteAccess) {
|
|
||||||
const directoryAccessResult = await getDirectoryAccess(context.state.user.id, parentPath);
|
|
||||||
|
|
||||||
hasWriteAccess = directoryAccessResult.hasWriteAccess;
|
|
||||||
ownerUserId = directoryAccessResult.ownerUserId;
|
|
||||||
ownerParentPath = directoryAccessResult.ownerParentPath;
|
|
||||||
|
|
||||||
if (!hasWriteAccess) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdFile = await createFile(ownerUserId, ownerParentPath, name.trim(), await contents.arrayBuffer());
|
|
||||||
|
|
||||||
const newFiles = await getFiles(context.state.user.id, parentPath);
|
const newFiles = await getFiles(context.state.user.id, parentPath);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { FreshContextState } from '/lib/types.ts';
|
import { FreshContextState } from '/lib/types.ts';
|
||||||
import { getDirectoryAccess, getFile, getFileAccess } from '/lib/data/files.ts';
|
import { getFile } from '/lib/data/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
@@ -31,25 +31,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
currentPath = `${currentPath}/`;
|
currentPath = `${currentPath}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { hasWriteAccess, ownerUserId, ownerParentPath } = await getFileAccess(
|
const fileResult = await getFile(context.state.user.id, currentPath, decodeURIComponent(fileName));
|
||||||
context.state.user.id,
|
|
||||||
currentPath,
|
|
||||||
decodeURIComponent(fileName),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasWriteAccess) {
|
|
||||||
const directoryAccessResult = await getDirectoryAccess(context.state.user.id, currentPath);
|
|
||||||
|
|
||||||
hasWriteAccess = directoryAccessResult.hasWriteAccess;
|
|
||||||
ownerUserId = directoryAccessResult.ownerUserId;
|
|
||||||
ownerParentPath = directoryAccessResult.ownerParentPath;
|
|
||||||
|
|
||||||
if (!hasWriteAccess) {
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileResult = await getFile(ownerUserId, ownerParentPath, decodeURIComponent(fileName));
|
|
||||||
|
|
||||||
if (!fileResult.success) {
|
if (!fileResult.success) {
|
||||||
return new Response('Not Found', { status: 404 });
|
return new Response('Not Found', { status: 404 });
|
||||||
|
|||||||
Reference in New Issue
Block a user