Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add experimental webdav sync #287

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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: 2 additions & 2 deletions script/user-chart/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function parseChrome(content: string): UserCount {
if (!dateStr || !numberStr) {
return
}
// Replace '/' to '-', then rjust month and date
// Replace '/' to '-', then rjust month and date
const date = dateStr.split('/').map(str => rjust(str, 2, '0')).join('-')
const number = parseInt(numberStr)
date && number && (result[date] = number)
Expand All @@ -106,7 +106,7 @@ function parseEdge(content: string): UserCount {
if (!dateStr || !numberStr) {
return
}
// Replace '/' to '-', then rjust month and date
// Replace '/' to '-', then rjust month and date
const date = dateStr.split('/').map(str => rjust(str, 2, '0')).join('-')
const number = parseInt(numberStr)
date && number && (result[date] = number)
Expand Down
7 changes: 4 additions & 3 deletions src/api/gist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type Gist = BaseGist<File> & {

export type GistForm = BaseGist<FileForm>


const BASE_URL = 'https://api.github.com/gists'

/**
Expand Down Expand Up @@ -77,7 +78,7 @@ async function post<T, R>(token: string, uri: string, body?: R): Promise<T> {
* @returns detail of Gist
*/
export function getGist(token: string, id: string): Promise<Gist> {
return get(token, `/${id}`)
return get(token, `/${id}`);
}

/**
Expand Down Expand Up @@ -107,7 +108,7 @@ export async function findTarget(token: string, predicate: (gist: Gist) => boole
/**
* Create one gist
*
* @param token token
* @param token auth token
* @param gist gist info
* @returns gist info with id
*/
Expand Down Expand Up @@ -144,7 +145,7 @@ export async function getJsonFileContent<T>(file: File): Promise<T> {
return undefined
}
const response = await fetchGet(rawUrl)
return await response.json()
return await response.json() as Promise<T>
}

/**
Expand Down
13 changes: 13 additions & 0 deletions src/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,17 @@ export async function fetchDelete(url: string, option?: Option): Promise<Respons
console.error("Failed to fetch delete", e)
throw Error(e)
}
}

export async function fetchFind(url: string, option?: Option): Promise<Response> {
try {
const response = await fetch(url, {
...(option || {}),
method: "PROPFIND",
})
return response
} catch (e) {
console.error("Failed to fetch propfind", e)
throw Error(e)
}
}
8 changes: 4 additions & 4 deletions src/api/obsidian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ export type ObsidianRequestContext = {
auth: string
}

const authHeaders = (auth: string) => ({
"Authorization": `Bearer ${auth}`
const authHeaders = (token: string) => ({
"Authorization": `Bearer ${token}`
})

export async function listAllFiles(context: ObsidianRequestContext, dirPath: string): Promise<ObsidianResult<{ files: string[] }>> {
export async function listAllFiles(context: ObsidianRequestContext, dirPath: string) {
const { endpoint, auth } = context || {}
const url = `${endpoint || DEFAULT_ENDPOINT}/vault/${dirPath || ''}`
const response = await fetchGet(url, { headers: authHeaders(auth) })
return await response?.json()
return await response?.json() as (Promise<ObsidianResult<{ files: string[] }>>)
}

export async function updateFile(context: ObsidianRequestContext, filePath: string, content: string): Promise<void> {
Expand Down
33 changes: 33 additions & 0 deletions src/api/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export type Result<T> = {
success: boolean
} & (
| {
message: null;
data: T;
}
| {
message: string;
data: null;
}
)

export class ResultUtil {
static success<T>(data: T): Result<T> {
return {
success: true,
message: null,
data,
}
}

static error<T>(message?: string): Result<T> {
if (!message) {
message = "Unknown Error"
}
return {
success: false,
message,
data: null,
}
}
}
93 changes: 93 additions & 0 deletions src/api/webdav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2024 Hengyang Zhang
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/

// @ts-ignore
import { Base64 } from "js-base64"

import { fetchDelete, fetchFind, fetchGet, fetchPutText } from "./http"
import { ResultUtil, Result } from "./utils"

export const DEFAULT_ENDPOINT = "https://api.mahoo12138.cn/webdav"
export const INVALID_AUTH_CODE = 401
export const NOT_FOUND_CODE = 404

export interface WebDAVAuth {
username: string
password: string
}

export type WebdavRequestContext = {
endpoint?: string
auth: WebDAVAuth
}

const authHeaders = (auth: WebdavRequestContext["auth"]) => {
const authString = Base64.encode(auth.username + ":" + auth.password)
return {
Authorization: `Basic ${authString}`,
}
}

export async function listAllFiles(
context: WebdavRequestContext,
dirPath: string
): Promise<Result<string>> {
const { endpoint, auth } = context || {}
const url = `${endpoint || DEFAULT_ENDPOINT}/${dirPath || ""}`
const response = await fetchFind(url, {
headers: { ...authHeaders(auth), Depth: "1" },
})
let message = ""
if (response.status === 207) {
const data = await response.text()
return ResultUtil.success(data)
} else if (response.status === 404) {
message = "Not Found"
} else if (response.status === 401) {
message = "unAuth"
} else {
message = "Unknown error"
}
return ResultUtil.error(message)
}

export async function updateFile(
context: WebdavRequestContext,
filePath: string,
content: string
): Promise<void> {
const { endpoint, auth } = context || {}
const url = `${endpoint || DEFAULT_ENDPOINT}/${filePath}`
const headers = authHeaders(auth)
headers["Content-Type"] = "text/markdown"
await fetchPutText(url, content, { headers })
}

export async function getFileContent(
context: WebdavRequestContext,
filePath: string
): Promise<string | null> {
const { endpoint, auth } = context || {}
const url = `${endpoint || DEFAULT_ENDPOINT}/${filePath}`
const headers = authHeaders(auth)
const response = await fetchGet(url, { headers })
const { status } = response
return status >= 200 && status < 300 ? await response.text() : null
}

export async function deleteFile(
context: WebdavRequestContext,
filePath: string
): Promise<void> {
const { endpoint, auth } = context || {}
const url = `${endpoint || DEFAULT_ENDPOINT}/${filePath}`
const headers = authHeaders(auth)
const response = await fetchDelete(url, { headers })
if (response.status !== 200) {
console.log(`Failed to delete file of Obsidian. filePath=${filePath}`)
}
}
23 changes: 22 additions & 1 deletion src/app/components/Option/components/BackupOption/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import type { PropType, Ref } from "vue"

import { t } from "@app/locale"
import { UploadFilled } from "@element-plus/icons-vue"
import { UploadFilled, RefreshRight } from "@element-plus/icons-vue"
import { ElButton, ElDivider, ElLoading, ElMessage, ElText } from "element-plus"
import { defineComponent, ref, watch } from "vue"
import metaService from "@service/meta-service"
Expand Down Expand Up @@ -51,9 +51,30 @@ const _default = defineComponent({
queryLastTime()
watch(() => props.type, queryLastTime)

async function handleTest() {
const loading = ElLoading.service({ text: "Please wait...." });
try {
const { errorMsg } = await processor.checkAuth();
if (!errorMsg) {
ElMessage.success("Valid!");
} else {
ElMessage.error(errorMsg);
}
} finally {
loading.close();
}
}

return () => <div>
<ElDivider />
<div class="backup-footer">
<ElButton
onClick={handleTest}
icon={<RefreshRight />}
style={{ marginRight: "12px" }}
>
{t((msg) => msg.option.backup.test)}
</ElButton>
<Clear />
<Download />
<ElButton
Expand Down
Loading