Compare commits
18
Commits
0.5.23
...
refactor-2026
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39711d117c | ||
|
|
60bad0fcd3 | ||
|
|
46f86c0091 | ||
|
|
af79356e58 | ||
|
|
6a6f192942 | ||
|
|
87059c37a0 | ||
|
|
c96de5ccbc | ||
|
|
f92bcd630d | ||
|
|
48b4f7e19c | ||
|
|
9dde1bcdc8 | ||
|
|
34db181af0 | ||
|
|
6631a9c5eb | ||
|
|
e39edcea70 | ||
|
|
9ac7ecf828 | ||
|
|
50c08400ff | ||
|
|
7bc2fad88b | ||
|
|
6dc1c28361 | ||
|
|
2220f511e3 |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"all": true,
|
||||
"include": ["src/**/*.ts", "pro/src/**/*.ts"],
|
||||
"exclude": ["tests/**", "pro/tests/**", "**/*.d.ts", "**/langs/**"],
|
||||
"reports-dir": "coverage"
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: BuildCI
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -9,12 +6,27 @@ on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: jdx/mise-action@v2
|
||||
- run: npm ci
|
||||
- name: Biome check
|
||||
run: npx @biomejs/biome ci .
|
||||
|
||||
test:
|
||||
name: Test
|
||||
needs: lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
environment: env-for-buildci
|
||||
|
||||
env:
|
||||
DROPBOX_APP_KEY: ${{secrets.DROPBOX_APP_KEY}}
|
||||
ONEDRIVE_CLIENT_ID: ${{secrets.ONEDRIVE_CLIENT_ID}}
|
||||
@@ -31,15 +43,8 @@ jobs:
|
||||
YANDEXDISK_CLIENT_SECRET: ${{secrets.YANDEXDISK_CLIENT_SECRET}}
|
||||
KOOFR_CLIENT_ID: ${{secrets.KOOFR_CLIENT_ID}}
|
||||
KOOFR_CLIENT_SECRET: ${{secrets.KOOFR_CLIENT_SECRET}}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- name: Checkout codes
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Checkout LFS file list
|
||||
@@ -53,17 +58,26 @@ jobs:
|
||||
${{ runner.os }}-lfs-
|
||||
- name: Git LFS Pull
|
||||
run: git lfs pull
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
- uses: jdx/mise-action@v2
|
||||
- run: npm ci
|
||||
- name: Testes com cobertura
|
||||
run: npm run test:coverage
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: my-dist
|
||||
path: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
name: coverage
|
||||
path: coverage/
|
||||
|
||||
security:
|
||||
name: Security
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: jdx/mise-action@v2
|
||||
- run: npm ci
|
||||
- name: npm audit
|
||||
run: npm audit --audit-level=high
|
||||
- name: Biome lint (regras de seguranca)
|
||||
run: npx @biomejs/biome lint .
|
||||
+13
-5
@@ -1,14 +1,14 @@
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
.idea/
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
package-lock.json
|
||||
node_modules/
|
||||
pnpm-lock.yaml
|
||||
|
||||
# build
|
||||
main.js
|
||||
*.main.js
|
||||
*.js.map
|
||||
|
||||
# obsidian
|
||||
@@ -17,5 +17,13 @@ data.json
|
||||
# debug
|
||||
logs.txt
|
||||
|
||||
# hidden files
|
||||
.*
|
||||
# coverage
|
||||
coverage/
|
||||
|
||||
# env / secrets
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Guia para Claude Code (claude.ai/code) trabalhar neste fork.
|
||||
|
||||
## Sobre o projeto
|
||||
|
||||
Fork de [`remotely-save/remotely-save`](https://github.com/remotely-save/remotely-save), plugin Obsidian que sincroniza vaults com WebDAV/S3/Dropbox/OneDrive/etc. Upstream com main branch parada desde 2024-11. Este fork retoma manutenção.
|
||||
|
||||
Detalhes técnicos completos em:
|
||||
- `docs/ARCHITECTURE.md` — abstração `FakeFs`, fluxo de sync, state em IndexedDB
|
||||
- `docs/DEVELOPMENT.md` — setup mise, comandos npm
|
||||
- `docs/CONTRIBUTING.md` — workflow, convenções, política de cobertura
|
||||
|
||||
## Stack
|
||||
|
||||
- TypeScript 5.5 + Node 24.15.0 (pin via `mise.toml`)
|
||||
- npm 11 (pin via `packageManager`)
|
||||
- Webpack (build principal) + esbuild (alternativo)
|
||||
- Biome (lint + format)
|
||||
- Mocha + chai-as-promised + tsx (testes)
|
||||
- c8 (cobertura)
|
||||
|
||||
## Convenções específicas deste fork
|
||||
|
||||
### Commits
|
||||
|
||||
**Inglês**, não pt-BR (alinhar com upstream). Imperativo, primeira letra maiúscula, ~72 caracteres na primeira linha. Corpo opcional com bullets `-`. Citar PR upstream quando aplicável (`based on upstream PR #1094`).
|
||||
|
||||
### Dependências
|
||||
|
||||
- **Só versões com ≥ 30 dias** publicadas no npm registry — checar `time` antes de subir
|
||||
- Pinar **exato** (sem `^`/`~`) quando você mexer numa versão
|
||||
- Usar `overrides` para forçar transitivas seguras quando o upstream não atualizou
|
||||
|
||||
### Cobertura
|
||||
|
||||
Floor atual: **8%** (ratchet — só sobe). Configurado em `package.json:scripts.test:coverage` com `c8 --check-coverage --lines=N`. Ao subir cobertura, atualizar `N` no script.
|
||||
|
||||
### Lint
|
||||
|
||||
Pode usar `// biome-ignore <rule>: <razão>` para suprimir regra com justificativa explícita. Sem suprimir silenciosamente.
|
||||
|
||||
### Estilo de código
|
||||
|
||||
- **SRP**: uma coisa por função, uma responsabilidade por módulo.
|
||||
- **Nomes**: específicos e únicos. Evitar `data`, `handler`, `Manager`. Preferir nomes que retornem menos de 5 hits num grep do codebase.
|
||||
- **Tipos**: explícitos. Sem `any`, sem `Dict`, sem funções sem tipo.
|
||||
- **Funções**: ≤ 50 linhas. Dividir se passar.
|
||||
- **Controle de fluxo**: early returns, no máximo 2 níveis de aninhamento.
|
||||
- **Exceções**: incluir o valor ofensor e o formato esperado na mensagem.
|
||||
|
||||
## CI (`.github/workflows/ci.yml`)
|
||||
|
||||
Padrão `Lint → Test → Security`, cada um com responsabilidade única:
|
||||
|
||||
| Stage | Comando | Timeout |
|
||||
|---|---|---|
|
||||
| Lint | `npx @biomejs/biome ci .` | 3min |
|
||||
| Test | `npm run test:coverage` | 10min |
|
||||
| Security | `npm audit --audit-level=high` + `npx @biomejs/biome lint .` | 5min |
|
||||
|
||||
Build de produção fica em `release.yml` (não no CI de PR). Local: `npm run build`.
|
||||
|
||||
## Cherry-pick de PRs do upstream
|
||||
|
||||
```bash
|
||||
git fetch upstream pull/<N>/head:pr-<N>
|
||||
git cherry-pick <commits>
|
||||
```
|
||||
|
||||
Validar local (`npm run format && npm test && npm run build`) antes de commit. Mencionar o PR no commit message.
|
||||
|
||||
## Cuidados ao mexer
|
||||
|
||||
- `pro/src/account.ts:200-201` tem código intencional para liberar pro features sem servidor. Não remover.
|
||||
- `src/main.ts` (~2k LOC) e `src/settings.ts` (~3k LOC) são funções gigantes — refactors em passos pequenos.
|
||||
- `pro/src/sync.ts:doActualSync` é o coração. **Sem testes** atualmente. Qualquer mudança aqui precisa teste novo.
|
||||
- Backends `fs*.ts` seguem `FakeFs`. Não criar novo sem implementar a interface completa.
|
||||
|
||||
## Filosofia
|
||||
|
||||
- AI como pair programmer: narrar decisões técnicas (especialmente em tsconfig/build/CI). Perguntar em vez de assumir. Explicar trade-off.
|
||||
- Mudança incremental: commits pequenos e bisect-friendly.
|
||||
- Cada feature ou bugfix inclui teste.
|
||||
@@ -122,6 +122,7 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
||||
- [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md)
|
||||
- [Nginx (`ngx_http_dav_module`, `nginx-dav-ext-module`, with Docker)](./docs/remote_services/webdav_nginx/README.md)
|
||||
- [Apache (with Docker)](./docs/remote_services/webdav_apache/README.md)
|
||||
- [Caddy with `http.handlers.webdav` module](./docs/remote_services/webdav_caddy/README.md)
|
||||
- Very old version of Obsidian needs [configuring CORS](./docs/remote_services/webdav_general/webav_cors.md).
|
||||
- Your data would be synced to a `${vaultName}` sub folder on your webdav server.
|
||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
- [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md)
|
||||
- [Nginx (`ngx_http_dav_module`, `nginx-dav-ext-module`, with Docker)](./docs/remote_services/webdav_nginx/README.md)
|
||||
- [Apache (with Docker)](./docs/remote_services/webdav_apache/README.md)
|
||||
- [Caddy with `http.handlers.webdav` module](./docs/remote_services/webdav_caddy/README.md)
|
||||
- 非常旧版本的Obsidian需要[配置 CORS](./docs/remote_services/webdav_general/webav_cors.md)。
|
||||
- 你的数据会同步到你的webdav服务器上的 `${vaultName}` 子文件夹。
|
||||
- 基于密码的端到端加密也是可以的。但请注意,**vault 名称本身未加密**。
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["main.js"]
|
||||
"ignore": ["main.js", "*.main.js", "coverage/**", "node_modules/**"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Arquitetura
|
||||
|
||||
Plugin Obsidian (TypeScript) que sincroniza vaults com serviços de armazenamento remoto (WebDAV, S3, Dropbox, OneDrive, etc).
|
||||
|
||||
## Estrutura
|
||||
|
||||
```
|
||||
src/ código público (MIT)
|
||||
├── main.ts entry point, estende Plugin do Obsidian (~2k linhas)
|
||||
├── settings.ts UI de settings (~3k linhas)
|
||||
├── fsAll.ts abstração FakeFs — interface comum dos backends
|
||||
├── fsGetter.ts roteamento backend → instância (switch por tipo)
|
||||
├── fs<Backend>.ts implementações: S3, WebDAV, Dropbox, OneDrive, Webdis, Local, Encrypt
|
||||
├── localdb.ts IndexedDB via LocalForage — state local
|
||||
├── configPersist.ts serialização da config no disco do plugin
|
||||
├── i18n.ts Mustache + moment, idiomas em src/langs/
|
||||
└── ...
|
||||
|
||||
pro/ código tier pago (PolyForm Strict License)
|
||||
├── src/
|
||||
│ ├── sync.ts motor de sincronização (~2k linhas, doActualSync + dispatchDecision)
|
||||
│ ├── fs<Backend>.ts GoogleDrive, Box, pCloud, YandexDisk, Koofr, Azure, OnedriveFull
|
||||
│ └── ...
|
||||
└── tests/
|
||||
|
||||
tests/ testes Mocha (estrutura básica, sem cobertura do core)
|
||||
docs/ documentação
|
||||
.github/workflows/ CI
|
||||
```
|
||||
|
||||
## Abstração FakeFs
|
||||
|
||||
Todo backend implementa a classe abstrata `FakeFs` (`src/fsAll.ts`):
|
||||
|
||||
```typescript
|
||||
abstract class FakeFs {
|
||||
walk() / walkPartial() / stat() / mkdir() / writeFile()
|
||||
readFile() / rename() / rm()
|
||||
checkConnect() / getUserDisplayName() / revokeAuth() / allowEmptyFile()
|
||||
}
|
||||
```
|
||||
|
||||
`fsGetter.ts` faz o roteamento via switch — ao adicionar backend novo, registrar nele.
|
||||
|
||||
## Fluxo de sincronização
|
||||
|
||||
1. `main.ts` dispara sync (manual, auto-sync, evento de save)
|
||||
2. `pro/src/sync.ts:doActualSync()` carrega árvore local + remota
|
||||
3. Diff 3-way usando snapshot anterior (`prevSyncRecordsTbl` em IndexedDB)
|
||||
4. `dispatchDecision()` aplica decisão por entry (copy, merge, delete, skip)
|
||||
5. Conflitos: 3-way merge via `node-diff3` (apenas Markdown < 1MB, tier pro)
|
||||
6. Erros agregados em `AggregateError`; falha até 3x antes de abortar
|
||||
|
||||
## State
|
||||
|
||||
LocalForage (IndexedDB) com 8 tabelas em `src/localdb.ts`:
|
||||
|
||||
- `prevSyncRecordsTbl` — snapshot da última sync
|
||||
- `fileContentHistoryTbl` — histórico de conteúdo (smart conflict)
|
||||
- `syncPlansTbl` — log das operações planejadas
|
||||
- `versionTbl`, `loggerOutputTbl`, `profilerResultsTbl` — metadados
|
||||
|
||||
Config persiste via API do Obsidian em `<vault>/.obsidian/plugins/remotely-save/data.json`,
|
||||
com obfuscação base64 reverse (decorativa, não criptográfica).
|
||||
|
||||
## Pontos de atenção
|
||||
|
||||
- **mtime no WebDAV**: padrão WebDAV não define mtime — Nextcloud expõe via header custom (`OC-LastModified`). Inconsistência pode causar falsa detecção de modificação.
|
||||
- **OAuth2 duplicado**: cada backend implementa fluxo próprio (~100-300 linhas duplicadas). Sem extração comum.
|
||||
- **Sem retry HTTP**: 429/503 quebram sync. PR upstream #1034 propõe retry para WebDAV.
|
||||
- **141 `: any` residuais**: concentrados em callbacks de error e payloads OAuth2.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Contribuindo
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Branch a partir de `master`: `git checkout -b <tipo>/<descrição>`
|
||||
- `feat/` — feature nova
|
||||
- `fix/` — bugfix
|
||||
- `refactor/` — refactor sem mudança de comportamento
|
||||
- `docs/` — só documentação
|
||||
- `chore/` — config, build, CI
|
||||
2. Commits pequenos, foco único
|
||||
3. Antes de abrir PR: `npm run format && npm test && npm run build`
|
||||
4. Abrir PR descrevendo o porquê (não o quê — o diff mostra o quê)
|
||||
|
||||
## Mensagens de commit
|
||||
|
||||
- Inglês, imperativo (alinhado com o upstream)
|
||||
- Primeira linha curta (~72 caracteres), inicia com letra maiúscula
|
||||
- Corpo opcional em lista com `- ` detalhando o porquê
|
||||
|
||||
Exemplo:
|
||||
|
||||
```
|
||||
Add WebDAV retry on 429/503
|
||||
|
||||
- new retryWithBackoff helper in src/fsWebdav.ts
|
||||
- apply to walk/stat/writeFile methods
|
||||
- based on upstream PR #1034
|
||||
```
|
||||
|
||||
## Cherry-pick de PRs do upstream
|
||||
|
||||
Upstream: https://github.com/remotely-save/remotely-save
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/remotely-save/remotely-save.git
|
||||
git fetch upstream pull/<N>/head:pr-<N>
|
||||
git cherry-pick <commits-de-pr-N>
|
||||
```
|
||||
|
||||
Citar o PR upstream na mensagem de commit (`baseado no PR upstream #<N>`).
|
||||
|
||||
## Estilo de código
|
||||
|
||||
- Biome formata e linta (`npm run format`)
|
||||
- TypeScript strict — evitar `any`
|
||||
- Funções idealmente 4–20 linhas; refatorar se passar
|
||||
- Nomes específicos, não `data`/`handler`/`Manager`
|
||||
- Sem comentários óbvios — explicar só o porquê quando não-trivial
|
||||
- Early returns (no máximo 2 níveis de aninhamento)
|
||||
|
||||
## Testes
|
||||
|
||||
- Toda feature ou bugfix inclui teste
|
||||
- Localização: `tests/` para src/, `pro/tests/` para pro/
|
||||
- Comando: `npm run test:coverage`
|
||||
|
||||
### Política de cobertura (ratchet)
|
||||
|
||||
Cobertura mínima de linhas é um **piso que só sobe**. Atualmente em `8%`
|
||||
(via `c8 --check-coverage --lines=8` no script `test:coverage`).
|
||||
|
||||
Quando você adicionar testes que elevam a cobertura geral, **suba o piso**
|
||||
no script `test:coverage` para o novo valor (truncado). Nunca abaixar.
|
||||
|
||||
Meta de longo prazo: 70% (padrão code-standards). Áreas zero-cobertura
|
||||
prioritárias: `src/main.ts`, `src/settings.ts`, backends `fs*.ts`.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Desenvolvimento
|
||||
|
||||
## Setup
|
||||
|
||||
Pré-requisito: [mise](https://mise.jdx.dev/) instalado.
|
||||
|
||||
```bash
|
||||
mise install # instala Node conforme mise.toml
|
||||
npm ci # instala dependências do lockfile
|
||||
```
|
||||
|
||||
## Comandos
|
||||
|
||||
```bash
|
||||
npm run dev # webpack watch (dev)
|
||||
npm run build # build de produção (webpack)
|
||||
npm run dev2 # esbuild watch (alternativo)
|
||||
npm run build2 # build de produção (esbuild + tsc check)
|
||||
npm test # mocha (tests/ + pro/tests/)
|
||||
npm run format # biome check --write
|
||||
npm run clean # remove main.js
|
||||
```
|
||||
|
||||
## Instalar build local no Obsidian
|
||||
|
||||
Após `npm run build`, copiar para o vault:
|
||||
|
||||
```bash
|
||||
cp main.js manifest.json styles.css \
|
||||
/path/to/vault/.obsidian/plugins/remotely-save/
|
||||
```
|
||||
|
||||
Reload do Obsidian (Ctrl+R) para carregar a nova versão.
|
||||
|
||||
## Testes
|
||||
|
||||
Framework: Mocha + chai-as-promised + tsx (sem transpile separado).
|
||||
|
||||
Estrutura:
|
||||
|
||||
```
|
||||
tests/ testes do código público (src/)
|
||||
pro/tests/ testes do código pro/
|
||||
```
|
||||
|
||||
Comando: `npm test`.
|
||||
|
||||
## Lint/format
|
||||
|
||||
Biome (`biome.json` na raiz):
|
||||
|
||||
```bash
|
||||
npm run format # corrige automaticamente
|
||||
npx @biomejs/biome check # só verifica
|
||||
```
|
||||
|
||||
## Release
|
||||
|
||||
Disparado via tag git. Workflow `.github/workflows/release.yml` constrói e publica artefatos:
|
||||
|
||||
```bash
|
||||
git tag x.y.z
|
||||
git push origin x.y.z
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
# Caddy with `http.handlers.webdav` module
|
||||
|
||||
> modified from the instruction from @cyruz-git in https://github.com/remotely-save/remotely-save/issues/825
|
||||
|
||||
## Link
|
||||
|
||||
<https://caddyserver.com/download?package=github.com%2Fmholt%2Fcaddy-webdav>
|
||||
|
||||
## Steps
|
||||
|
||||
1. Download caddy with webdav module from <https://caddyserver.com/download?package=github.com%2Fmholt%2Fcaddy-webdav>. Or you can install Caddy then install the plugins.
|
||||
2. Create a folder for storing webdav server files. Like `/usr/local/mywebdav`.
|
||||
3. Create a `Caddyfile` (yeah the file name itself is `Caddyfile`.) like this:
|
||||
```caddy
|
||||
:8080 {
|
||||
route /dav/* {
|
||||
root /usr/local/mywebdav
|
||||
basicauth {
|
||||
# Username "Bob", password "hiccup"
|
||||
Bob $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
|
||||
}
|
||||
uri strip_prefix /dav
|
||||
webdav
|
||||
}
|
||||
}
|
||||
```
|
||||
The password hash is generated like [this](https://caddyserver.com/docs/caddyfile/directives/basic_auth).
|
||||
4. In Remotely Save, setup:
|
||||
* address `http://localhost:8080/dav/`
|
||||
* username `Bob`
|
||||
* password `hiccup`
|
||||
* auth type: `basic`
|
||||
5. Check the connection and sync!
|
||||
+1
-1
@@ -57,7 +57,7 @@ esbuild
|
||||
inject: ["./esbuild.injecthelper.mjs"],
|
||||
format: "cjs",
|
||||
// watch: !prod, // no longer valid in esbuild 0.17
|
||||
target: "es2016",
|
||||
target: "es2020",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "remotely-save",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.5.23",
|
||||
"version": "0.5.25",
|
||||
"minAppVersion": "0.13.21",
|
||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "remotely-save",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.5.23",
|
||||
"version": "0.5.25",
|
||||
"minAppVersion": "0.13.21",
|
||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
|
||||
Generated
+11423
File diff suppressed because it is too large
Load Diff
+17
-8
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"name": "remotely-save",
|
||||
"version": "0.5.23",
|
||||
"version": "0.5.25",
|
||||
"description": "This is yet another sync plugin for Obsidian app.",
|
||||
"packageManager": "npm@11.12.1",
|
||||
"engines": {
|
||||
"node": ">=24.15.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev2": "node esbuild.config.mjs --watch",
|
||||
"build2": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
@@ -9,7 +13,8 @@
|
||||
"dev": "webpack --mode development --watch",
|
||||
"format": "npx @biomejs/biome check --write .",
|
||||
"clean": "npx rimraf main.js",
|
||||
"test": "mocha --import=tsx 'tests/**/*.ts' 'pro/tests/**/*.ts'"
|
||||
"test": "mocha --import=tsx 'tests/**/*.ts' 'pro/tests/**/*.ts'",
|
||||
"test:coverage": "c8 --check-coverage --lines=8 --reporter=text --reporter=lcov --reporter=html npm test"
|
||||
},
|
||||
"browser": {
|
||||
"path": "path-browserify",
|
||||
@@ -21,11 +26,17 @@
|
||||
"vm": false
|
||||
},
|
||||
"source": "main.ts",
|
||||
"overrides": {
|
||||
"elliptic": "6.6.1",
|
||||
"diff": "9.0.0",
|
||||
"serialize-javascript": "7.0.5"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.8.3",
|
||||
"c8": "11.0.0",
|
||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||
"@types/chai": "^4.3.16",
|
||||
"@types/chai-as-promised": "^7.1.8",
|
||||
@@ -34,16 +45,15 @@
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/mustache": "^4.2.5",
|
||||
"@types/node": "^20.14.12",
|
||||
"@types/node": "24.12.2",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"builtin-modules": "^4.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"esbuild": "^0.23.0",
|
||||
"esbuild": "0.28.0",
|
||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||
"jsdom": "^24.1.1",
|
||||
"mocha": "^10.7.0",
|
||||
"npm-check-updates": "^16.14.20",
|
||||
"mocha": "11.7.5",
|
||||
"obsidian": "^1.5.7",
|
||||
"openapi-typescript": "^7.1.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
@@ -70,12 +80,11 @@
|
||||
"@smithy/protocol-http": "^4.1.0",
|
||||
"@smithy/querystring-builder": "^3.0.3",
|
||||
"acorn": "^8.12.1",
|
||||
"aggregate-error": "^5.0.0",
|
||||
"assert": "^2.1.0",
|
||||
"aws-crt": "^1.21.3",
|
||||
"box-typescript-sdk-gen": "^1.3.0",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"crypto-browserify": "3.12.1",
|
||||
"dropbox": "^10.34.0",
|
||||
"emoji-regex": "^10.3.0",
|
||||
"http-status-codes": "^2.3.0",
|
||||
|
||||
@@ -173,6 +173,32 @@ export const getAndSaveProFeatures = async (
|
||||
pluginVersion: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any> | undefined
|
||||
) => {
|
||||
const features = [
|
||||
"feature-smart_conflict",
|
||||
"feature-onedrive_full",
|
||||
"feature-google_drive",
|
||||
"feature-box",
|
||||
"feature-pcloud",
|
||||
"feature-yandex_disk",
|
||||
"feature-koofr",
|
||||
"feature-azure_blob_storage",
|
||||
];
|
||||
|
||||
const res = {
|
||||
proFeatures: features.map(
|
||||
(i) =>
|
||||
({
|
||||
featureName: i,
|
||||
enableAtTimeMs: 1e12,
|
||||
expireAtTimeMs: 3e12,
|
||||
}) as FeatureInfo
|
||||
),
|
||||
};
|
||||
config.enabledProFeatures = res.proFeatures;
|
||||
await saveUpdatedConfigFunc?.();
|
||||
return res;
|
||||
|
||||
// biome-ignore lint/correctness/noUnreachable: original upstream flow kept for reference (the fake-license shortcut above is intentional)
|
||||
const access = await getAccessToken(config, saveUpdatedConfigFunc);
|
||||
|
||||
const resp1 = await fetch(`${site}/api/v1/pro/list`, {
|
||||
|
||||
@@ -37,8 +37,8 @@ export type PRO_FEATURE_TYPE =
|
||||
|
||||
export interface FeatureInfo {
|
||||
featureName: PRO_FEATURE_TYPE;
|
||||
enableAtTimeMs: bigint;
|
||||
expireAtTimeMs: bigint;
|
||||
enableAtTimeMs: number;
|
||||
expireAtTimeMs: number;
|
||||
}
|
||||
|
||||
export interface ProConfig {
|
||||
|
||||
@@ -78,7 +78,7 @@ const fromBlobPropsToEntity = (
|
||||
|
||||
let hash: undefined | string = undefined;
|
||||
if (props.contentMD5 !== undefined) {
|
||||
hash = arrayBufferToHex(props.contentMD5.buffer);
|
||||
hash = arrayBufferToHex(props.contentMD5.buffer as ArrayBuffer);
|
||||
}
|
||||
|
||||
const entity: Entity = {
|
||||
|
||||
+143
-67
@@ -2,7 +2,6 @@
|
||||
// https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow
|
||||
// https://developers.google.com/identity/protocols/oauth2/web-server
|
||||
|
||||
import { entries } from "lodash";
|
||||
import * as mime from "mime-types";
|
||||
import { requestUrl } from "obsidian";
|
||||
import PQueue from "p-queue";
|
||||
@@ -177,6 +176,7 @@ export class FakeFsGoogleDrive extends FakeFs {
|
||||
keyToGDEntity: Record<string, GDEntity>;
|
||||
|
||||
baseDirID: string;
|
||||
ready = false;
|
||||
|
||||
constructor(
|
||||
googleDriveConfig: GoogleDriveConfig,
|
||||
@@ -199,13 +199,17 @@ export class FakeFsGoogleDrive extends FakeFs {
|
||||
await this._getAccessToken();
|
||||
|
||||
// check vault folder exists
|
||||
if (this.vaultFolderExists) {
|
||||
// pass
|
||||
} else {
|
||||
const q = encodeURIComponent(
|
||||
`name='${this.remoteBaseDir}' and mimeType='application/vnd.google-apps.folder' and trashed=false`
|
||||
if (!this.vaultFolderExists) {
|
||||
const q = `name='${this.remoteBaseDir}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
|
||||
const url = new URL("https://www.googleapis.com/drive/v3/files");
|
||||
url.searchParams.set("q", q);
|
||||
url.searchParams.set("pageSize", "1000");
|
||||
url.searchParams.set(
|
||||
"fields",
|
||||
"kind,nextPageToken," +
|
||||
"files(kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum)"
|
||||
);
|
||||
const url: string = `https://www.googleapis.com/drive/v3/files?q=${q}&pageSize=1000&fields=kind,nextPageToken,files(kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum)`;
|
||||
url.searchParams.set("orderBy", "modifiedTime desc");
|
||||
const k = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -274,8 +278,16 @@ export class FakeFsGoogleDrive extends FakeFs {
|
||||
/**
|
||||
* https://developers.google.com/drive/api/reference/rest/v3/files/list
|
||||
*/
|
||||
async walk(): Promise<Entity[]> {
|
||||
async walk(): Promise<GDEntity[]> {
|
||||
await this._init();
|
||||
|
||||
// const allFiles = await this._listAllFiles();
|
||||
// this.keyToGDEntity = allFiles.reduce((p, c) => {
|
||||
// p[c.keyRaw] = c;
|
||||
// return p;
|
||||
// }, {} as Record<string, GDEntity>); // rebuild cache
|
||||
|
||||
// return allFiles;
|
||||
const allFiles: GDEntity[] = [];
|
||||
|
||||
// bfs
|
||||
@@ -289,39 +301,23 @@ export class FakeFsGoogleDrive extends FakeFs {
|
||||
throw error;
|
||||
});
|
||||
|
||||
let parents = [
|
||||
{
|
||||
id: this.baseDirID, // special init, from already created root folder ID
|
||||
folderPath: "",
|
||||
},
|
||||
];
|
||||
while (parents.length !== 0) {
|
||||
const children: typeof parents = [];
|
||||
for (const { id, folderPath } of parents) {
|
||||
queue.add(async () => {
|
||||
const filesUnderFolder = await this._walkFolder(id, folderPath);
|
||||
const newWalkTask = (id: string, folderPath: string) => {
|
||||
return async () => {
|
||||
const filesUnderFolder = await this._listFolder(id, folderPath);
|
||||
for (const f of filesUnderFolder) {
|
||||
allFiles.push(f);
|
||||
if (f.isFolder) {
|
||||
// keyRaw itself already has a tailing slash, no more slash here
|
||||
// keyRaw itself also already has full path
|
||||
const child = {
|
||||
id: f.id,
|
||||
folderPath: f.keyRaw,
|
||||
queue.add(newWalkTask(f.id, f.keyRaw));
|
||||
}
|
||||
}
|
||||
};
|
||||
// console.debug(
|
||||
// `looping result of _walkFolder(${id},${folderPath}), adding child=${JSON.stringify(
|
||||
// child
|
||||
// )}`
|
||||
// );
|
||||
children.push(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
queue.add(newWalkTask(this.baseDirID, "")); // special init, from already created root folder ID
|
||||
|
||||
await queue.onIdle();
|
||||
parents = children;
|
||||
}
|
||||
|
||||
// console.debug(`in the end of walk:`);
|
||||
// console.debug(allFiles);
|
||||
@@ -329,25 +325,97 @@ export class FakeFsGoogleDrive extends FakeFs {
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
async _walkFolder(parentID: string, parentFolderPath: string) {
|
||||
async _listAllFiles(): Promise<GDEntity[]> {
|
||||
const allFileRes: File[] = [];
|
||||
let nextPageToken = "";
|
||||
|
||||
do {
|
||||
const q = `'${this.baseDirID}' in parents and trashed=false`;
|
||||
const url = new URL("https://www.googleapis.com/drive/v3/files");
|
||||
url.searchParams.set("q", q);
|
||||
url.searchParams.set("pageSize", "1000");
|
||||
url.searchParams.set(
|
||||
"fields",
|
||||
"kind,nextPageToken,files(kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum)"
|
||||
);
|
||||
url.searchParams.set("orderBy", "modifiedTime");
|
||||
url.searchParams.set("pageToken", nextPageToken);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this._getAccessToken()}`,
|
||||
},
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
throw Error(`Error on list all files`);
|
||||
}
|
||||
|
||||
const fileRes = await res.json();
|
||||
(fileRes.files as File[]).forEach((i) => allFileRes.push(i));
|
||||
|
||||
nextPageToken = fileRes.nextPageToken;
|
||||
} while (nextPageToken !== undefined);
|
||||
|
||||
const allFolderRes = allFileRes.filter(
|
||||
(i) => i.mimeType === FOLDER_MIME_TYPE
|
||||
);
|
||||
|
||||
const allFolders = [
|
||||
{
|
||||
id: this.baseDirID, // special init, from already created root folder ID
|
||||
folderPath: "",
|
||||
},
|
||||
];
|
||||
|
||||
for (const targetFolder of allFolders) {
|
||||
allFolderRes
|
||||
.filter((i) => i.parents?.includes(targetFolder.id))
|
||||
.forEach((i) => {
|
||||
allFolders.push({
|
||||
id: i.id!,
|
||||
folderPath: `${targetFolder}${i.name!}/`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const allFiles: GDEntity[] = [];
|
||||
|
||||
for (const file of allFileRes) {
|
||||
if (!file.parents) continue;
|
||||
file.parents.forEach((parent) => {
|
||||
const folder = allFolders.find((folder) => folder.id === parent);
|
||||
if (!folder) return;
|
||||
const entity = fromFileToGDEntity(file, folder.id, folder.folderPath);
|
||||
allFiles.push(entity);
|
||||
});
|
||||
}
|
||||
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
async _listFolder(parentID: string, parentFolderPath: string) {
|
||||
// console.debug(
|
||||
// `input of single level: parentID=${parentID}, parentFolderPath=${parentFolderPath}`
|
||||
// );
|
||||
const filesOneLevel: GDEntity[] = [];
|
||||
let nextPageToken: string | undefined = undefined;
|
||||
let nextPageToken = "";
|
||||
if (parentID === undefined || parentID === "" || parentID === "root") {
|
||||
// we should never start from root
|
||||
// because we encapsulate the vault inside a folder
|
||||
throw Error(`something goes wrong walking folder`);
|
||||
}
|
||||
do {
|
||||
const q = encodeURIComponent(
|
||||
`'${parentID}' in parents and trashed=false`
|
||||
const q = `'${parentID}' in parents and trashed=false`;
|
||||
const url = new URL("https://www.googleapis.com/drive/v3/files");
|
||||
url.searchParams.set("q", q);
|
||||
url.searchParams.set("pageSize", "1000");
|
||||
url.searchParams.set(
|
||||
"fields",
|
||||
"kind,nextPageToken,files(kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum)"
|
||||
);
|
||||
const pageToken =
|
||||
nextPageToken !== undefined ? `&pageToken=${nextPageToken}` : "";
|
||||
|
||||
const url: string = `https://www.googleapis.com/drive/v3/files?q=${q}&pageSize=1000&fields=kind,nextPageToken,files(kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum)${pageToken}`;
|
||||
url.searchParams.set("orderBy", "modifiedTime");
|
||||
url.searchParams.set("pageToken", nextPageToken);
|
||||
|
||||
const k = await fetch(url, {
|
||||
method: "GET",
|
||||
@@ -377,7 +445,7 @@ export class FakeFsGoogleDrive extends FakeFs {
|
||||
|
||||
async walkPartial(): Promise<Entity[]> {
|
||||
await this._init();
|
||||
const filesInLevel = await this._walkFolder(this.baseDirID, "");
|
||||
const filesInLevel = await this._listFolder(this.baseDirID, "");
|
||||
return filesInLevel;
|
||||
}
|
||||
|
||||
@@ -508,6 +576,8 @@ export class FakeFsGoogleDrive extends FakeFs {
|
||||
// "xxx" => []
|
||||
// "xxx/yyy/zzz.md" => ["xxx", "xxx/yyy"]
|
||||
const folderLevels = getFolderLevels(key);
|
||||
console.log(key);
|
||||
console.log(folderLevels);
|
||||
if (folderLevels.length === 0) {
|
||||
// root
|
||||
parentID = this.baseDirID;
|
||||
@@ -522,34 +592,42 @@ export class FakeFsGoogleDrive extends FakeFs {
|
||||
parentID = this.keyToGDEntity[parentFolderPath].id;
|
||||
}
|
||||
|
||||
const fileItself = key.split("/").pop()!;
|
||||
const targetFileId = this.keyToGDEntity[key]?.id;
|
||||
|
||||
if (content.byteLength <= 5 * 1024 * 1024) {
|
||||
const formData = new FormData();
|
||||
const fileItself = key.split("/").pop()!;
|
||||
const meta: any = {
|
||||
name: fileItself,
|
||||
modifiedTime: unixTimeToStr(mtime, true),
|
||||
createdTime: unixTimeToStr(ctime, true),
|
||||
parents: [parentID],
|
||||
};
|
||||
if (!targetFileId) meta.parents = [parentID];
|
||||
|
||||
if (content.byteLength <= 5 * 1024 * 1024) {
|
||||
const formData = new FormData();
|
||||
formData.append(
|
||||
"metadata",
|
||||
new Blob([JSON.stringify(meta)], {
|
||||
new Blob(targetFileId ? [] : [JSON.stringify(meta)], {
|
||||
type: "application/json; charset=UTF-8",
|
||||
})
|
||||
);
|
||||
|
||||
formData.append("media", new Blob([content], { type: contentType }));
|
||||
|
||||
const res = await fetch(
|
||||
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum",
|
||||
{
|
||||
method: "POST",
|
||||
const url = new URL("https://www.googleapis.com/upload/drive/v3/files");
|
||||
if (targetFileId) url.pathname += `/${targetFileId}`;
|
||||
url.searchParams.set("uploadType", "multipart");
|
||||
url.searchParams.set(
|
||||
"fields",
|
||||
"kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum"
|
||||
);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: targetFileId ? "PATCH" : "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this._getAccessToken()}`,
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
});
|
||||
if (res.status !== 200 && res.status !== 201) {
|
||||
throw Error(`create file ${key} failed! meta=${JSON.stringify(meta)}`);
|
||||
}
|
||||
@@ -564,13 +642,7 @@ export class FakeFsGoogleDrive extends FakeFs {
|
||||
this.keyToGDEntity[key] = entity;
|
||||
return entity;
|
||||
} else {
|
||||
const meta: any = {
|
||||
name: fileItself,
|
||||
modifiedTime: unixTimeToStr(mtime, true),
|
||||
createdTime: unixTimeToStr(ctime, true),
|
||||
parents: [parentID],
|
||||
};
|
||||
const bodyStr = JSON.stringify(meta);
|
||||
const bodyStr = targetFileId ? "" : JSON.stringify(meta);
|
||||
const headers: HeadersInit = {
|
||||
Authorization: `Bearer ${await this._getAccessToken()}`,
|
||||
"Content-Type": "application/json",
|
||||
@@ -578,14 +650,18 @@ export class FakeFsGoogleDrive extends FakeFs {
|
||||
"X-Upload-Content-Type": contentType,
|
||||
"X-Upload-Content-Length": `${content.byteLength}`,
|
||||
};
|
||||
const res = await fetch(
|
||||
"https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&fields=kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum",
|
||||
{
|
||||
method: "POST",
|
||||
const url = new URL("https://www.googleapis.com/upload/drive/v3/files");
|
||||
if (targetFileId) url.pathname += `/${targetFileId}`;
|
||||
url.searchParams.set("uploadType", "resumable");
|
||||
url.searchParams.set(
|
||||
"fields",
|
||||
"kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum"
|
||||
);
|
||||
const res = await fetch(url, {
|
||||
method: targetFileId ? "PATCH" : "POST",
|
||||
headers: headers,
|
||||
body: bodyStr,
|
||||
}
|
||||
);
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
throw Error(
|
||||
`create resumable file ${key} failed! meta=${JSON.stringify(
|
||||
|
||||
@@ -575,7 +575,7 @@ export class FakeFsOnedriveFull extends FakeFs {
|
||||
*/
|
||||
async _putUint8ArrayByRange(
|
||||
pathFragOrig: string,
|
||||
payload: Uint8Array,
|
||||
payload: Uint8Array<ArrayBuffer>,
|
||||
rangeStart: number,
|
||||
rangeEnd: number,
|
||||
size: number
|
||||
|
||||
@@ -268,7 +268,7 @@
|
||||
"settings_azureblobstorage_parts": "Parts Concurrency",
|
||||
"settings_azureblobstorage_parts_desc": "Large files are split into small parts to upload. How many parts do you want to upload in parallel at most?",
|
||||
"settings_azureblobstorage_generatefolderobject": "Generate Folder Object Or Not",
|
||||
"settings_azureblobstorage_generatefolderobject_desc": "Azure Blob Storage doesn't have \"real\" folder. If you set \"Generate\" here, the plugin will upload a zero-byte object endding with \"/\" to represent the folder. By default, the plugin skips generating folder object.",
|
||||
"settings_azureblobstorage_generatefolderobject_desc": "Azure Blob Storage doesn't have \"real\" folder. If you set \"Generate\" here, the plugin will upload a zero-byte object ending with \"/\" to represent the folder. By default, the plugin skips generating folder object.",
|
||||
"settings_azureblobstorage_generatefolderobject_notgenerate": "Not generate (default)",
|
||||
"settings_azureblobstorage_generatefolderobject_generate": "Generate",
|
||||
"settings_azureblobstorage_connect_succ": "Great! We can connect to Azure Blob Storage!",
|
||||
|
||||
+10
-5
@@ -1,5 +1,3 @@
|
||||
// biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
|
||||
import AggregateError from "aggregate-error";
|
||||
import PQueue from "p-queue";
|
||||
import XRegExp from "xregexp";
|
||||
import type {
|
||||
@@ -305,7 +303,14 @@ export const getSkipItemsByList = (
|
||||
}
|
||||
|
||||
// we deal with explictly allow list secondly, apply them to PARENTS if possible
|
||||
const enableAllowMode = skipOrNotResults[allPotentialKeys[0]].enableAllowMode;
|
||||
let enableAllowMode = false;
|
||||
if (
|
||||
allPotentialKeys.length > 0 &&
|
||||
allPotentialKeys[0] !== undefined &&
|
||||
skipOrNotResults[allPotentialKeys[0]] !== undefined
|
||||
) {
|
||||
enableAllowMode = skipOrNotResults[allPotentialKeys[0]].enableAllowMode;
|
||||
}
|
||||
if (enableAllowMode) {
|
||||
for (let index = 0; index < sortedKeys.length; index++) {
|
||||
// reverse order, long(deep) to short(shadow), ascending
|
||||
@@ -1923,7 +1928,7 @@ export async function syncer(
|
||||
) => any,
|
||||
callbackSyncProcess?: any
|
||||
) {
|
||||
console.info(`startting sync.`);
|
||||
console.info(`starting sync.`);
|
||||
markIsSyncingFunc(true);
|
||||
|
||||
let everythingOk = true;
|
||||
@@ -2082,6 +2087,6 @@ export async function syncer(
|
||||
await ribboonFunc?.(triggerSource, step);
|
||||
await statusBarFunc?.(triggerSource, step, everythingOk);
|
||||
|
||||
console.info(`endding sync.`);
|
||||
console.info(`ending sync.`);
|
||||
markIsSyncingFunc(false);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ const getKeyIVFromPassword = async (
|
||||
const k2 = await window.crypto.subtle.deriveBits(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
salt: salt,
|
||||
salt: salt as Uint8Array<ArrayBuffer>,
|
||||
iterations: rounds,
|
||||
hash: "SHA-256",
|
||||
},
|
||||
|
||||
+48
-1
@@ -252,6 +252,10 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
||||
// why?? /drive/root:/Apps/Graph
|
||||
const FIFTH_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/Graph\//g;
|
||||
|
||||
// why again?? /drive/root:/Apps/Graph 1
|
||||
const SIXTH_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/Graph 1\//g;
|
||||
const SIXTH_COMMON_PREFIX_REGEX_V2 = /^\/drive\/root:\/[^\/]+\/Graph%201\//g;
|
||||
|
||||
// or the root is absolute path /Livefolders,
|
||||
// e.g.: /Livefolders/应用/remotely-save/${remoteBaseDir}
|
||||
const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g;
|
||||
@@ -278,6 +282,11 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
||||
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
|
||||
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_COMMON_PREFIX_REGEX);
|
||||
const matchFifthPrefixRes = fullPathOriginal.match(FIFTH_COMMON_PREFIX_REGEX);
|
||||
const matchSixthPrefixRes = fullPathOriginal.match(SIXTH_COMMON_PREFIX_REGEX);
|
||||
const matchSixthV2PrefixRes = fullPathOriginal.match(
|
||||
SIXTH_COMMON_PREFIX_REGEX_V2
|
||||
);
|
||||
|
||||
const matchSecondPrefixRes = fullPathOriginal.match(
|
||||
SECOND_COMMON_PREFIX_REGEX
|
||||
);
|
||||
@@ -317,6 +326,40 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
||||
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||
}
|
||||
|
||||
// sixth
|
||||
else if (
|
||||
matchSixthPrefixRes !== null &&
|
||||
fullPathOriginal.startsWith(`${matchSixthPrefixRes[0]}${remoteBaseDir}`)
|
||||
) {
|
||||
const foundPrefix = `${matchSixthPrefixRes[0]}${remoteBaseDir}`;
|
||||
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||
} else if (
|
||||
matchSixthPrefixRes !== null &&
|
||||
fullPathOriginal.startsWith(
|
||||
`${matchSixthPrefixRes[0]}${remoteBaseDirEncoded}`
|
||||
)
|
||||
) {
|
||||
const foundPrefix = `${matchSixthPrefixRes[0]}${remoteBaseDirEncoded}`;
|
||||
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||
}
|
||||
|
||||
// sixth v2
|
||||
else if (
|
||||
matchSixthV2PrefixRes !== null &&
|
||||
fullPathOriginal.startsWith(`${matchSixthV2PrefixRes[0]}${remoteBaseDir}`)
|
||||
) {
|
||||
const foundPrefix = `${matchSixthV2PrefixRes[0]}${remoteBaseDir}`;
|
||||
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||
} else if (
|
||||
matchSixthV2PrefixRes !== null &&
|
||||
fullPathOriginal.startsWith(
|
||||
`${matchSixthV2PrefixRes[0]}${remoteBaseDirEncoded}`
|
||||
)
|
||||
) {
|
||||
const foundPrefix = `${matchSixthV2PrefixRes[0]}${remoteBaseDirEncoded}`;
|
||||
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||
}
|
||||
|
||||
// second
|
||||
else if (
|
||||
matchSecondPrefixRes !== null &&
|
||||
@@ -374,6 +417,8 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
||||
fullPathOriginal=${fullPathOriginal}
|
||||
matchFirstPrefixRes=${matchFirstPrefixRes}
|
||||
matchFifthPrefixRes=${matchFifthPrefixRes}
|
||||
matchSixthPrefixRes=${matchSixthPrefixRes}
|
||||
matchSixthV2PrefixRes=${matchSixthV2PrefixRes}
|
||||
matchSecondPrefixRes=${matchSecondPrefixRes}
|
||||
matchThirdPrefixRes=${matchThirdPrefixRes}
|
||||
${constructFromDriveItemToEntityError(x)}`
|
||||
@@ -388,6 +433,8 @@ ${constructFromDriveItemToEntityError(x)}`
|
||||
fullPathOriginal=${fullPathOriginal}
|
||||
matchFirstPrefixRes=${matchFirstPrefixRes}
|
||||
matchFifthPrefixRes=${matchFifthPrefixRes}
|
||||
matchSixthPrefixRes=${matchSixthPrefixRes}
|
||||
matchSixthV2PrefixRes=${matchSixthV2PrefixRes}
|
||||
matchSecondPrefixRes=${matchSecondPrefixRes}
|
||||
matchThirdPrefixRes=${matchThirdPrefixRes}
|
||||
${constructFromDriveItemToEntityError(x)}`
|
||||
@@ -682,7 +729,7 @@ export class FakeFsOnedrive extends FakeFs {
|
||||
*/
|
||||
async _putUint8ArrayByRange(
|
||||
pathFragOrig: string,
|
||||
payload: Uint8Array,
|
||||
payload: Uint8Array<ArrayBuffer>,
|
||||
rangeStart: number,
|
||||
rangeEnd: number,
|
||||
size: number
|
||||
|
||||
@@ -22,8 +22,6 @@ import {
|
||||
import { requestTimeout } from "@smithy/fetch-http-handler/dist-es/request-timeout";
|
||||
import { type HttpRequest, HttpResponse } from "@smithy/protocol-http";
|
||||
import { buildQueryString } from "@smithy/querystring-builder";
|
||||
// biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
|
||||
import AggregateError from "aggregate-error";
|
||||
import * as mime from "mime-types";
|
||||
import { Platform, type RequestUrlParam, requestUrl } from "obsidian";
|
||||
import PQueue from "p-queue";
|
||||
|
||||
+33
-12
@@ -138,7 +138,6 @@ if (VALID_REQURL) {
|
||||
);
|
||||
}
|
||||
|
||||
import isEqual from "lodash/isEqual";
|
||||
// @ts-ignore
|
||||
// biome-ignore lint: we want to ts-ignore the next line
|
||||
import { AuthType, BufferLike, createClient } from "webdav/dist/web/index.js";
|
||||
@@ -169,23 +168,37 @@ const getWebdavPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
|
||||
return key;
|
||||
};
|
||||
|
||||
/**
|
||||
* sometimes the path startswith /../../......
|
||||
* we want to make sure the path is compatible
|
||||
*/
|
||||
const stripLeadingPath = (x: string) => {
|
||||
let y = x;
|
||||
while (y.startsWith("/..")) {
|
||||
y = y.slice("/..".length);
|
||||
}
|
||||
return y;
|
||||
};
|
||||
|
||||
const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
|
||||
const strippedFileOrFolderPath = stripLeadingPath(fileOrFolderPath);
|
||||
if (
|
||||
!(
|
||||
fileOrFolderPath === `/${remoteBaseDir}` ||
|
||||
fileOrFolderPath.startsWith(`/${remoteBaseDir}/`)
|
||||
strippedFileOrFolderPath === `/${remoteBaseDir}` ||
|
||||
strippedFileOrFolderPath.startsWith(`/${remoteBaseDir}/`)
|
||||
)
|
||||
) {
|
||||
throw Error(
|
||||
`"${fileOrFolderPath}" doesn't starts with "/${remoteBaseDir}/"`
|
||||
`"${fileOrFolderPath}" after stripping doesn't starts with "/${remoteBaseDir}/"`
|
||||
);
|
||||
}
|
||||
|
||||
return fileOrFolderPath.slice(`/${remoteBaseDir}/`.length);
|
||||
const result = strippedFileOrFolderPath.slice(`/${remoteBaseDir}/`.length);
|
||||
return result;
|
||||
};
|
||||
|
||||
const fromWebdavItemToEntity = (x: FileStat, remoteBaseDir: string): Entity => {
|
||||
let key = getNormPath(x.filename, remoteBaseDir);
|
||||
|
||||
if (x.type === "directory" && !key.endsWith("/")) {
|
||||
key = `${key}/`;
|
||||
}
|
||||
@@ -447,15 +460,17 @@ export class FakeFsWebdav extends FakeFs {
|
||||
// console.debug(itemsToFetchChunks);
|
||||
const subContents = [] as FileStat[];
|
||||
for (const singleChunk of itemsToFetchChunks) {
|
||||
const r = singleChunk.map((x) => {
|
||||
return this.client.getDirectoryContents(x, {
|
||||
const r = singleChunk.map(async (x) => {
|
||||
let k = (await this.client.getDirectoryContents(x, {
|
||||
deep: false,
|
||||
details: false /* no need for verbose details here */,
|
||||
// TODO: to support .obsidian,
|
||||
// we need to load all files including dot,
|
||||
// anyway to reduce the resources?
|
||||
// glob: "/**" /* avoid dot files by using glob */,
|
||||
}) as Promise<FileStat[]>;
|
||||
})) as FileStat[];
|
||||
k = k.filter((sub) => stripLeadingPath(sub.filename) !== x);
|
||||
return k;
|
||||
});
|
||||
const r3 = await Promise.all(r);
|
||||
for (const r4 of r3) {
|
||||
@@ -477,7 +492,7 @@ export class FakeFsWebdav extends FakeFs {
|
||||
const f = subContents[i];
|
||||
contents.push(f);
|
||||
if (f.type === "directory") {
|
||||
q.push(f.filename);
|
||||
q.push(stripLeadingPath(f.filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -495,7 +510,11 @@ export class FakeFsWebdav extends FakeFs {
|
||||
}
|
||||
)) as FileStat[];
|
||||
}
|
||||
return contents.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir));
|
||||
|
||||
const result = contents
|
||||
.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir))
|
||||
.filter((x) => x.keyRaw !== "/");
|
||||
return result;
|
||||
}
|
||||
|
||||
async walkPartial(): Promise<Entity[]> {
|
||||
@@ -508,7 +527,9 @@ export class FakeFsWebdav extends FakeFs {
|
||||
details: false /* no need for verbose details here */,
|
||||
}
|
||||
)) as FileStat[];
|
||||
return contents.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir));
|
||||
return contents
|
||||
.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir))
|
||||
.filter((x) => x.keyRaw !== "/");
|
||||
}
|
||||
|
||||
async stat(key: string): Promise<Entity> {
|
||||
|
||||
+3
-3
@@ -31,7 +31,7 @@
|
||||
"protocol_dropbox_connect_succ_revoke": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
||||
"protocol_dropbox_connect_fail": "Something went wrong from response from Dropbox. Maybe the network connection is not good. Maybe you rejected the auth?",
|
||||
"protocol_dropbox_connect_unknown": "Do not know how to deal with the callback: {{params}}",
|
||||
"protocol_dropbox_no_modal": "You are not startting Dropbox connection from the settings page. Abort.",
|
||||
"protocol_dropbox_no_modal": "You are not starting Dropbox connection from the settings page. Abort.",
|
||||
"protocol_onedrive_connecting": "Connecting to OneDrive...\nPlease DO NOT close this modal.",
|
||||
"protocol_onedrive_connect_succ_revoke": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
||||
"protocol_onedrive_connect_fail": "Something went wrong from response from OneDrive. Maybe you rejected the auth?",
|
||||
@@ -110,7 +110,7 @@
|
||||
"modal_onedriverevokeauth_clean_button": "Clean",
|
||||
"modal_onedriverevokeauth_clean_notice": "Cleaned!",
|
||||
"modal_onedriverevokeauth_clean_fail": "Something goes wrong while revoking.",
|
||||
"modal_syncconfig_attn": "Attention 1/2: This only syncs (copies) the whole Obsidian config dir, not other startting-with-dot folders or files. Except for ignoring folders .git and node_modules, it also doesn't understand the meaning of sub-files and sub-folders inside the config dir.\nAttention 2/2: After the config dir is synced, plugins settings might be corrupted, and Obsidian might need to be restarted to load the new settings.\nIf you are agreed to take your own risk, please click the following second confirm button.",
|
||||
"modal_syncconfig_attn": "Attention 1/2: This only syncs (copies) the whole Obsidian config dir, not other starting-with-dot folders or files. Except for ignoring folders .git and node_modules, it also doesn't understand the meaning of sub-files and sub-folders inside the config dir.\nAttention 2/2: After the config dir is synced, plugins settings might be corrupted, and Obsidian might need to be restarted to load the new settings.\nIf you are agreed to take your own risk, please click the following second confirm button.",
|
||||
"modal_syncconfig_secondconfirm": "The Second Confirm To Enable.",
|
||||
"modal_syncconfig_notice": "You've enabled syncing config folder!",
|
||||
"modal_qr_shortdesc": "This exports (partial) settings.\nYou can use another device to scan this qrcode.\nOr, you can click the button to copy the special uri and paste it into another device's web browser or Remotely Save Import Setting.",
|
||||
@@ -194,7 +194,7 @@
|
||||
"settings_s3_reverse_proxy_no_sign_url": "S3 Reverse Proxy (No Sign) Url (experimental)",
|
||||
"settings_s3_reverse_proxy_no_sign_url_desc": "S3 reverse proxy url without signature. This is useful if you use a revers proxy but do not change the original credential signature. No http(s):// prefix. Leave it blank if you don't know what it is.",
|
||||
"settings_s3_generatefolderobject": "Generate Folder Object Or Not",
|
||||
"settings_s3_generatefolderobject_desc": "S3 doesn't have \"real\" folder. If you set \"Generate\" here (or use old version), the plugin will upload a zero-byte object endding with \"/\" to represent the folder. In the new version, the plugin skips generating folder object by default.",
|
||||
"settings_s3_generatefolderobject_desc": "S3 doesn't have \"real\" folder. If you set \"Generate\" here (or use old version), the plugin will upload a zero-byte object ending with \"/\" to represent the folder. In the new version, the plugin skips generating folder object by default.",
|
||||
"settings_s3_generatefolderobject_notgenerate": "Not generate (default)",
|
||||
"settings_s3_generatefolderobject_generate": "Generate",
|
||||
"settings_s3_connect_succ": "Great! The bucket can be accessed.",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
|
||||
import AggregateError from "aggregate-error";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import throttle from "lodash/throttle";
|
||||
import { FileText, RefreshCcw, RotateCcw, createElement } from "lucide";
|
||||
|
||||
+7
-4
@@ -88,9 +88,12 @@ export const mkdirpInVault = async (thePath: string, vault: Vault) => {
|
||||
* @returns ArrayBuffer
|
||||
*/
|
||||
export const bufferToArrayBuffer = (
|
||||
b: Buffer | Uint8Array | ArrayBufferView
|
||||
b: Buffer | Uint8Array<ArrayBuffer> | ArrayBufferView
|
||||
) => {
|
||||
return b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
|
||||
return b.buffer.slice(
|
||||
b.byteOffset,
|
||||
b.byteOffset + b.byteLength
|
||||
) as ArrayBuffer;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -279,7 +282,7 @@ export const reverseString = (x: string) => {
|
||||
};
|
||||
|
||||
export interface SplitRange {
|
||||
partNum: number; // startting from 1
|
||||
partNum: number; // starting from 1
|
||||
start: number;
|
||||
end: number; // exclusive
|
||||
}
|
||||
@@ -316,7 +319,7 @@ export const getTypeName = (obj: any) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Startting from 1
|
||||
* starting from 1
|
||||
* @param x
|
||||
* @returns
|
||||
*/
|
||||
|
||||
+2
-2
@@ -8,13 +8,13 @@
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
// "allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"lib": ["dom", "es5", "scripthost", "es2015", "webworker"]
|
||||
"lib": ["dom", "es5", "scripthost", "es2015", "es2021", "webworker"]
|
||||
},
|
||||
"include": ["src/global.d.ts", "**/*.ts"]
|
||||
}
|
||||
|
||||
@@ -65,6 +65,11 @@ module.exports = {
|
||||
new webpack.ProvidePlugin({
|
||||
process: "process/browser",
|
||||
}),
|
||||
// Strip `node:` URI prefix so resolve.fallback (browserify shims) applies.
|
||||
// Required because some deps (AWS smithy, glob, ...) use `node:url` etc.
|
||||
new webpack.NormalModuleReplacementPlugin(/^node:/, (resource) => {
|
||||
resource.request = resource.request.replace(/^node:/, "");
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
|
||||
Reference in New Issue
Block a user