Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
* text=auto eol=lf

*.bat text eol=crlf
*.cmd text eol=crlf
74 changes: 56 additions & 18 deletions apps/cent/src/api/endpoints/web-dav/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { WebDAVEdit } from "@/components/modal/web-dav";
import { Scheduler } from "@/database/scheduler";
import { BillIndexedDBStorage } from "@/database/storage";
import type { Bill } from "@/ledger/type";
import type { Bill, GlobalMeta } from "@/ledger/type";
import { createTidal } from "@/tidal";
import { checkWebDAVConfig, createWebDAVSyncer } from "@/tidal/web-dav";
import {
checkWebDAVConfig,
createWebDAVSyncer,
fetchWebDAVUserIds,
} from "@/tidal/web-dav";
import type { ZenPost } from "@/zen/types";
import type { SyncEndpointFactory } from "../type";

Expand All @@ -29,6 +33,12 @@ type WebDAVPrivateMeta = {
_webDAVUserAliases?: string[];
};

type WebDAVMeta = WebDAVPrivateMeta & GlobalMeta;

const restoreUserIdType = (userId: string) => {
return /^\d+$/.test(userId) ? Number(userId) : userId;
};

export const WebDAVEndpoint: SyncEndpointFactory = {
type: "webdav",
name: "webdav",
Expand All @@ -47,6 +57,24 @@ export const WebDAVEndpoint: SyncEndpointFactory = {
);
return Promise.reject(error);
});

const userIds = await fetchWebDAVUserIds({
remoteUrl: remote,
username: data.username,
password: data.password,
proxy: data.proxy,
});
if (userIds.length === 0) {
data.userId = crypto.randomUUID();
} else if (userIds.length === 1) {
data.userId = restoreUserIdType(userIds[0]);
} else {
const selection = await modal.webDavUser({ userIds });
data.userId =
selection.type === "new"
? crypto.randomUUID()
: restoreUserIdType(selection.userId);
}
},
});
if (!auth) {
Expand All @@ -69,6 +97,8 @@ export const WebDAVEndpoint: SyncEndpointFactory = {
password: auth.password,
remoteUrl: remote,
proxy: auth.proxy,
userId: auth.userId,
displayName: auth.customUserName,
};
const repo = createTidal<Bill>({
storageFactory: (name) => new BillIndexedDBStorage(`book-${name}`),
Expand Down Expand Up @@ -118,19 +148,12 @@ export const WebDAVEndpoint: SyncEndpointFactory = {
await repo.detach();
await zenRepo.detach();
},
getUserInfo: async () => {
const Me = {
id: auth.customUserName || auth.username,
name: auth.customUserName || auth.username,
avatar_url: "/icon.png",
};
return Me;
},
getUserInfo: repo.getUserInfo,
getCollaborators: async (id) => {
const aliases = await getUserAliases(id);
const Me = {
id: auth.username,
name: auth.username,
id: (auth.userId ?? auth.username) as unknown as string,
name: auth.customUserName || auth.username,
avatar_url: "/icon.png",
};
const users = [
Expand All @@ -153,17 +176,32 @@ export const WebDAVEndpoint: SyncEndpointFactory = {
createBook: repo.create,
initBook: async (name) => {
await Promise.all([repo.init(name), zenRepo.init(name)]);
repo.getMeta(name).then((meta?: WebDAVPrivateMeta) => {
repo.getMeta(name).then((meta?: WebDAVMeta) => {
const customUserName = auth.customUserName;
if (!customUserName) {
return;
const userId = auth.userId;
const newMeta = meta ?? ({} as WebDAVMeta);
let needsUpdate = false;

if (userId !== undefined && !newMeta.personal?.[userId]) {
newMeta.personal = {
...newMeta.personal,
[userId]: {},
};
needsUpdate = true;
}
if (!meta?._webDAVUserAliases?.includes(customUserName)) {
const newMeta = meta ?? {};

if (
customUserName &&
!newMeta._webDAVUserAliases?.includes(customUserName)
) {
newMeta._webDAVUserAliases = [
...(meta?._webDAVUserAliases ?? []),
...(newMeta._webDAVUserAliases ?? []),
customUserName,
];
needsUpdate = true;
}

if (needsUpdate) {
repo.batch(name, [
{
type: "meta",
Expand Down
3 changes: 3 additions & 0 deletions apps/cent/src/components/modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { LoadingProvider, loading } from "./loading";
import { PromptProvider, prompt } from "./prompt";
import { S3AuthProvider, showS3Auth } from "./s3";
import { showWebDAVAuth, WebDAVAuthProvider } from "./web-dav";
import { showWebDAVUserSelect, WebDAVUserSelectProvider } from "./web-dav-user";

export function ModalProvider() {
return (
<>
<PromptProvider />
<LoadingProvider />
<WebDAVAuthProvider />
<WebDAVUserSelectProvider />
<S3AuthProvider />
</>
);
Expand All @@ -19,6 +21,7 @@ const modal = {
loading,
prompt,
webDavAuth: showWebDAVAuth,
webDavUser: showWebDAVUserSelect,
s3Auth: showS3Auth,
toast,
};
Expand Down
73 changes: 73 additions & 0 deletions apps/cent/src/components/modal/web-dav-user.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useState } from "react";
import { useIntl } from "@/locale";
import createConfirmProvider from "../confirm";
import { Button } from "../ui/button";

export type WebDAVUserSelection =
| { type: "existing"; userId: string }
| { type: "new" };

export type WebDAVUserSelectOptions = {
userIds: string[];
};

const NEW_USER = "__new_webdav_user__";

const WebDAVUserSelectForm = ({
edit,
onCancel,
onConfirm,
}: {
edit?: WebDAVUserSelectOptions;
onCancel?: () => void;
onConfirm?: (selection: WebDAVUserSelection) => void;
}) => {
const t = useIntl();
const [selected, setSelected] = useState(edit?.userIds[0] ?? NEW_USER);

return (
<div className="flex flex-col gap-4 p-4">
<div className="font-medium">{t("web-dav-select-user")}</div>
<select
className="h-9 rounded-md border bg-background px-3 text-sm"
value={selected}
onChange={(event) => setSelected(event.target.value)}
>
{edit?.userIds.map((userId) => (
<option key={userId} value={userId}>
{userId}
</option>
))}
<option value={NEW_USER}>{t("web-dav-create-user")}</option>
</select>

<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={onCancel}>
{t("cancel")}
</Button>
<Button
onClick={() =>
onConfirm?.(
selected === NEW_USER
? { type: "new" }
: { type: "existing", userId: selected },
)
}
>
{t("confirm")}
</Button>
</div>
</div>
);
};

export const [WebDAVUserSelectProvider, showWebDAVUserSelect] =
createConfirmProvider<WebDAVUserSelectOptions, WebDAVUserSelection>(
WebDAVUserSelectForm,
{
dialogTitle: "web-dav-select-user",
dialogModalClose: false,
contentClassName: "w-[360px] h-fit",
fade: true,
},
);
2 changes: 2 additions & 0 deletions apps/cent/src/components/modal/web-dav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type WebDAVEdit = {
password: string;
proxy?: string;
customUserName?: string;
userId?: string | number;
};

type LoadingState = Partial<WebDAVEdit> & {
Expand All @@ -35,6 +36,7 @@ export const createFormSchema = (t: any) =>
password: z.string(),
proxy: z.optional(z.string()),
customUserName: z.optional(z.string()),
userId: z.optional(z.union([z.string(), z.number()])),
});

const LoadingForm = ({
Expand Down
2 changes: 2 additions & 0 deletions apps/cent/src/locale/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@
"custom-user-name-tooltip": "When multiple people share the same account, you can set different nicknames to distinguish different users. This way you can see who created each record.",
"web-dav-proxy-url": "https://glink25.github.io/post/%E9%80%9A%E8%BF%87Web-DAV%E8%BF%9B%E8%A1%8CCent%E6%95%B0%E6%8D%AE%E5%90%8C%E6%AD%A5/#%E4%BD%BF%E7%94%A8%E4%BB%A3%E7%90%86",
"web-dav-custom-user": "https://glink25.github.io/post/%E9%80%9A%E8%BF%87Web-DAV%E8%BF%9B%E8%A1%8CCent%E6%95%B0%E6%8D%AE%E5%90%8C%E6%AD%A5/#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%94%A8%E6%88%B7",
"web-dav-select-user": "Select the user ID for WebDAV",
"web-dav-create-user": "Create a new user",
"sync-with-s3": "Sync with S3",
"s3-endpoint": "S3 Endpoint",
"s3-endpoint-required": "S3 Endpoint is required",
Expand Down
2 changes: 2 additions & 0 deletions apps/cent/src/locale/lang/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@
"custom-user-name-tooltip": "当多人共享同一个账号时,可以设置不同的昵称来区分不同用户。这样可以在记录中看到是谁创建的每条记录。",
"web-dav-proxy-url": "https://glink25.github.io/post/%E9%80%9A%E8%BF%87Web-DAV%E8%BF%9B%E8%A1%8CCent%E6%95%B0%E6%8D%AE%E5%90%8C%E6%AD%A5/#%E4%BD%BF%E7%94%A8%E4%BB%A3%E7%90%86",
"web-dav-custom-user": "https://glink25.github.io/post/%E9%80%9A%E8%BF%87Web-DAV%E8%BF%9B%E8%A1%8CCent%E6%95%B0%E6%8D%AE%E5%90%8C%E6%AD%A5/#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%94%A8%E6%88%B7",
"web-dav-select-user": "选择 WebDAV 使用的用户 ID",
"web-dav-create-user": "创建新用户",
"sync-with-s3": "通过 S3 进行数据同步",
"s3-endpoint": "S3 Endpoint",
"s3-endpoint-required": "S3 Endpoint 不能为空",
Expand Down
39 changes: 35 additions & 4 deletions apps/cent/src/tidal/web-dav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { shortId } from "@/database/id";
import { registerProxy } from "@/utils/fetch-proxy";
import type { AssetKey, FileLike, StoreStructure, Syncer } from ".";

type WebDAVConfig = {
export type WebDAVConfig = {
remoteUrl: string;
username?: string;
password?: string;
proxy?: string;
repoPrefix?: string;
entryName?: string;
baseDir?: string;
userId?: string | number;
displayName?: string;
};

const createClient = async (
Expand Down Expand Up @@ -286,7 +288,11 @@ export const createWebDAVSyncer = (cfg: WebDAVConfig): Syncer => {
await client.createDirectory(storePath, { recursive: true });
await client.putFileContents(
`${storePath}/meta.json`,
JSON.stringify({}),
JSON.stringify(
config.userId === undefined
? {}
: { personal: { [config.userId]: {} } },
),
);
await client.createDirectory(`${storePath}/assets`);
// return id and name similar to github.createStore
Expand All @@ -297,8 +303,11 @@ export const createWebDAVSyncer = (cfg: WebDAVConfig): Syncer => {
// WebDAV servers do not have standardized user API; return simple info based on username
return {
avatar_url: undefined,
name: config.username ?? "webdav-user",
id: config.username ?? "webdav-user",
name: config.displayName ?? config.username ?? "webdav-user",
id:
(config.userId as unknown as string | undefined) ??
config.username ??
"webdav-user",
} as unknown as UserInfo;
};

Expand Down Expand Up @@ -340,6 +349,28 @@ export const createWebDAVSyncer = (cfg: WebDAVConfig): Syncer => {
};
};

export const fetchWebDAVUserIds = async (
config: Pick<WebDAVConfig, "username" | "password" | "remoteUrl" | "proxy">,
) => {
const syncer = createWebDAVSyncer(config);
const storeNames = await syncer.fetchAllStore();
const userIds = new Set<string>();

for (const storeName of storeNames) {
const structure = await syncer.fetchStructure(storeName);
if (!structure.meta.path) continue;

const [metaFile] = await syncer.fetchContent(storeName, [
structure.meta,
]);
Object.keys(metaFile?.content?.personal ?? {}).forEach((userId) => {
userIds.add(userId);
});
}

return Array.from(userIds);
};

export const checkWebDAVConfig = async (
config: Pick<WebDAVConfig, "username" | "password" | "remoteUrl" | "proxy">,
) => {
Expand Down