Compare commits
No commits in common. "39711d117cd417a708f086848092be05dd310d1d" and "34db181af002f8d71ea0a87e7965abc57b294914" have entirely different histories.
39711d117c
...
34db181af0
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"all": true,
|
|
||||||
"include": ["src/**/*.ts", "pro/src/**/*.ts"],
|
|
||||||
"exclude": ["tests/**", "pro/tests/**", "**/*.d.ts", "**/langs/**"],
|
|
||||||
"reports-dir": "coverage"
|
|
||||||
}
|
|
||||||
@ -1,4 +1,7 @@
|
|||||||
name: CI
|
# 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
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -6,27 +9,12 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
build:
|
||||||
name: Lint
|
|
||||||
runs-on: ubuntu-latest
|
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
|
environment: env-for-buildci
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DROPBOX_APP_KEY: ${{secrets.DROPBOX_APP_KEY}}
|
DROPBOX_APP_KEY: ${{secrets.DROPBOX_APP_KEY}}
|
||||||
ONEDRIVE_CLIENT_ID: ${{secrets.ONEDRIVE_CLIENT_ID}}
|
ONEDRIVE_CLIENT_ID: ${{secrets.ONEDRIVE_CLIENT_ID}}
|
||||||
@ -43,8 +31,15 @@ jobs:
|
|||||||
YANDEXDISK_CLIENT_SECRET: ${{secrets.YANDEXDISK_CLIENT_SECRET}}
|
YANDEXDISK_CLIENT_SECRET: ${{secrets.YANDEXDISK_CLIENT_SECRET}}
|
||||||
KOOFR_CLIENT_ID: ${{secrets.KOOFR_CLIENT_ID}}
|
KOOFR_CLIENT_ID: ${{secrets.KOOFR_CLIENT_ID}}
|
||||||
KOOFR_CLIENT_SECRET: ${{secrets.KOOFR_CLIENT_SECRET}}
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout codes
|
||||||
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Checkout LFS file list
|
- name: Checkout LFS file list
|
||||||
@ -58,26 +53,17 @@ jobs:
|
|||||||
${{ runner.os }}-lfs-
|
${{ runner.os }}-lfs-
|
||||||
- name: Git LFS Pull
|
- name: Git LFS Pull
|
||||||
run: git lfs pull
|
run: git lfs pull
|
||||||
- uses: jdx/mise-action@v2
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
- run: npm ci
|
uses: actions/setup-node@v4
|
||||||
- name: Testes com cobertura
|
|
||||||
run: npm run test:coverage
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
if: always()
|
|
||||||
with:
|
with:
|
||||||
name: coverage
|
node-version: ${{ matrix.node-version }}
|
||||||
path: coverage/
|
- run: npm install
|
||||||
|
- run: npm test
|
||||||
security:
|
- run: npm run build
|
||||||
name: Security
|
- uses: actions/upload-artifact@v4
|
||||||
needs: test
|
with:
|
||||||
runs-on: ubuntu-latest
|
name: my-dist
|
||||||
timeout-minutes: 5
|
path: |
|
||||||
steps:
|
main.js
|
||||||
- uses: actions/checkout@v4
|
manifest.json
|
||||||
- uses: jdx/mise-action@v2
|
styles.css
|
||||||
- run: npm ci
|
|
||||||
- name: npm audit
|
|
||||||
run: npm audit --audit-level=high
|
|
||||||
- name: Biome lint (regras de seguranca)
|
|
||||||
run: npx @biomejs/biome lint .
|
|
||||||
18
.gitignore
vendored
18
.gitignore
vendored
@ -1,14 +1,14 @@
|
|||||||
# Intellij
|
# Intellij
|
||||||
*.iml
|
*.iml
|
||||||
.idea/
|
.idea
|
||||||
|
|
||||||
# npm
|
# npm
|
||||||
node_modules/
|
node_modules
|
||||||
|
package-lock.json
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|
||||||
# build
|
# build
|
||||||
main.js
|
main.js
|
||||||
*.main.js
|
|
||||||
*.js.map
|
*.js.map
|
||||||
|
|
||||||
# obsidian
|
# obsidian
|
||||||
@ -17,13 +17,5 @@ data.json
|
|||||||
# debug
|
# debug
|
||||||
logs.txt
|
logs.txt
|
||||||
|
|
||||||
# coverage
|
# hidden files
|
||||||
coverage/
|
.*
|
||||||
|
|
||||||
# env / secrets
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|||||||
84
CLAUDE.md
84
CLAUDE.md
@ -1,84 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@ -4,7 +4,7 @@
|
|||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignore": ["main.js", "*.main.js", "coverage/**", "node_modules/**"]
|
"ignore": ["main.js"]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
# 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`.
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
||||||
@ -57,7 +57,7 @@ esbuild
|
|||||||
inject: ["./esbuild.injecthelper.mjs"],
|
inject: ["./esbuild.injecthelper.mjs"],
|
||||||
format: "cjs",
|
format: "cjs",
|
||||||
// watch: !prod, // no longer valid in esbuild 0.17
|
// watch: !prod, // no longer valid in esbuild 0.17
|
||||||
target: "es2020",
|
target: "es2016",
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
sourcemap: prod ? false : "inline",
|
sourcemap: prod ? false : "inline",
|
||||||
treeShaking: true,
|
treeShaking: true,
|
||||||
|
|||||||
11423
package-lock.json
generated
11423
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@ -2,10 +2,6 @@
|
|||||||
"name": "remotely-save",
|
"name": "remotely-save",
|
||||||
"version": "0.5.25",
|
"version": "0.5.25",
|
||||||
"description": "This is yet another sync plugin for Obsidian app.",
|
"description": "This is yet another sync plugin for Obsidian app.",
|
||||||
"packageManager": "npm@11.12.1",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=24.15.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev2": "node esbuild.config.mjs --watch",
|
"dev2": "node esbuild.config.mjs --watch",
|
||||||
"build2": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
"build2": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
@ -13,8 +9,7 @@
|
|||||||
"dev": "webpack --mode development --watch",
|
"dev": "webpack --mode development --watch",
|
||||||
"format": "npx @biomejs/biome check --write .",
|
"format": "npx @biomejs/biome check --write .",
|
||||||
"clean": "npx rimraf main.js",
|
"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": {
|
"browser": {
|
||||||
"path": "path-browserify",
|
"path": "path-browserify",
|
||||||
@ -26,17 +21,11 @@
|
|||||||
"vm": false
|
"vm": false
|
||||||
},
|
},
|
||||||
"source": "main.ts",
|
"source": "main.ts",
|
||||||
"overrides": {
|
|
||||||
"elliptic": "6.6.1",
|
|
||||||
"diff": "9.0.0",
|
|
||||||
"serialize-javascript": "7.0.5"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.8.3",
|
"@biomejs/biome": "1.8.3",
|
||||||
"c8": "11.0.0",
|
|
||||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||||
"@types/chai": "^4.3.16",
|
"@types/chai": "^4.3.16",
|
||||||
"@types/chai-as-promised": "^7.1.8",
|
"@types/chai-as-promised": "^7.1.8",
|
||||||
@ -45,15 +34,16 @@
|
|||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.7",
|
||||||
"@types/mustache": "^4.2.5",
|
"@types/mustache": "^4.2.5",
|
||||||
"@types/node": "24.12.2",
|
"@types/node": "^20.14.12",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"builtin-modules": "^4.0.0",
|
"builtin-modules": "^4.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"esbuild": "0.28.0",
|
"esbuild": "^0.23.0",
|
||||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
"jsdom": "^24.1.1",
|
"jsdom": "^24.1.1",
|
||||||
"mocha": "11.7.5",
|
"mocha": "^10.7.0",
|
||||||
|
"npm-check-updates": "^16.14.20",
|
||||||
"obsidian": "^1.5.7",
|
"obsidian": "^1.5.7",
|
||||||
"openapi-typescript": "^7.1.0",
|
"openapi-typescript": "^7.1.0",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
@ -80,11 +70,12 @@
|
|||||||
"@smithy/protocol-http": "^4.1.0",
|
"@smithy/protocol-http": "^4.1.0",
|
||||||
"@smithy/querystring-builder": "^3.0.3",
|
"@smithy/querystring-builder": "^3.0.3",
|
||||||
"acorn": "^8.12.1",
|
"acorn": "^8.12.1",
|
||||||
|
"aggregate-error": "^5.0.0",
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
"aws-crt": "^1.21.3",
|
"aws-crt": "^1.21.3",
|
||||||
"box-typescript-sdk-gen": "^1.3.0",
|
"box-typescript-sdk-gen": "^1.3.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"crypto-browserify": "3.12.1",
|
"crypto-browserify": "^3.12.0",
|
||||||
"dropbox": "^10.34.0",
|
"dropbox": "^10.34.0",
|
||||||
"emoji-regex": "^10.3.0",
|
"emoji-regex": "^10.3.0",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
|
|||||||
@ -173,32 +173,6 @@ export const getAndSaveProFeatures = async (
|
|||||||
pluginVersion: string,
|
pluginVersion: string,
|
||||||
saveUpdatedConfigFunc: () => Promise<any> | undefined
|
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 access = await getAccessToken(config, saveUpdatedConfigFunc);
|
||||||
|
|
||||||
const resp1 = await fetch(`${site}/api/v1/pro/list`, {
|
const resp1 = await fetch(`${site}/api/v1/pro/list`, {
|
||||||
|
|||||||
@ -37,8 +37,8 @@ export type PRO_FEATURE_TYPE =
|
|||||||
|
|
||||||
export interface FeatureInfo {
|
export interface FeatureInfo {
|
||||||
featureName: PRO_FEATURE_TYPE;
|
featureName: PRO_FEATURE_TYPE;
|
||||||
enableAtTimeMs: number;
|
enableAtTimeMs: bigint;
|
||||||
expireAtTimeMs: number;
|
expireAtTimeMs: bigint;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProConfig {
|
export interface ProConfig {
|
||||||
|
|||||||
@ -78,7 +78,7 @@ const fromBlobPropsToEntity = (
|
|||||||
|
|
||||||
let hash: undefined | string = undefined;
|
let hash: undefined | string = undefined;
|
||||||
if (props.contentMD5 !== undefined) {
|
if (props.contentMD5 !== undefined) {
|
||||||
hash = arrayBufferToHex(props.contentMD5.buffer as ArrayBuffer);
|
hash = arrayBufferToHex(props.contentMD5.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
const entity: Entity = {
|
const entity: Entity = {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
// https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow
|
// https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow
|
||||||
// https://developers.google.com/identity/protocols/oauth2/web-server
|
// https://developers.google.com/identity/protocols/oauth2/web-server
|
||||||
|
|
||||||
|
import { entries } from "lodash";
|
||||||
import * as mime from "mime-types";
|
import * as mime from "mime-types";
|
||||||
import { requestUrl } from "obsidian";
|
import { requestUrl } from "obsidian";
|
||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
@ -176,7 +177,6 @@ export class FakeFsGoogleDrive extends FakeFs {
|
|||||||
keyToGDEntity: Record<string, GDEntity>;
|
keyToGDEntity: Record<string, GDEntity>;
|
||||||
|
|
||||||
baseDirID: string;
|
baseDirID: string;
|
||||||
ready = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
googleDriveConfig: GoogleDriveConfig,
|
googleDriveConfig: GoogleDriveConfig,
|
||||||
@ -199,17 +199,13 @@ export class FakeFsGoogleDrive extends FakeFs {
|
|||||||
await this._getAccessToken();
|
await this._getAccessToken();
|
||||||
|
|
||||||
// check vault folder exists
|
// check vault folder exists
|
||||||
if (!this.vaultFolderExists) {
|
if (this.vaultFolderExists) {
|
||||||
const q = `name='${this.remoteBaseDir}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
|
// pass
|
||||||
const url = new URL("https://www.googleapis.com/drive/v3/files");
|
} else {
|
||||||
url.searchParams.set("q", q);
|
const q = encodeURIComponent(
|
||||||
url.searchParams.set("pageSize", "1000");
|
`name='${this.remoteBaseDir}' and mimeType='application/vnd.google-apps.folder' and trashed=false`
|
||||||
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 desc");
|
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)`;
|
||||||
const k = await fetch(url, {
|
const k = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
@ -278,16 +274,8 @@ export class FakeFsGoogleDrive extends FakeFs {
|
|||||||
/**
|
/**
|
||||||
* https://developers.google.com/drive/api/reference/rest/v3/files/list
|
* https://developers.google.com/drive/api/reference/rest/v3/files/list
|
||||||
*/
|
*/
|
||||||
async walk(): Promise<GDEntity[]> {
|
async walk(): Promise<Entity[]> {
|
||||||
await this._init();
|
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[] = [];
|
const allFiles: GDEntity[] = [];
|
||||||
|
|
||||||
// bfs
|
// bfs
|
||||||
@ -301,23 +289,39 @@ export class FakeFsGoogleDrive extends FakeFs {
|
|||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
const newWalkTask = (id: string, folderPath: string) => {
|
let parents = [
|
||||||
return async () => {
|
{
|
||||||
const filesUnderFolder = await this._listFolder(id, folderPath);
|
id: this.baseDirID, // special init, from already created root folder ID
|
||||||
for (const f of filesUnderFolder) {
|
folderPath: "",
|
||||||
allFiles.push(f);
|
},
|
||||||
if (f.isFolder) {
|
];
|
||||||
// keyRaw itself already has a tailing slash, no more slash here
|
while (parents.length !== 0) {
|
||||||
// keyRaw itself also already has full path
|
const children: typeof parents = [];
|
||||||
queue.add(newWalkTask(f.id, f.keyRaw));
|
for (const { id, folderPath } of parents) {
|
||||||
|
queue.add(async () => {
|
||||||
|
const filesUnderFolder = await this._walkFolder(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,
|
||||||
|
};
|
||||||
|
// console.debug(
|
||||||
|
// `looping result of _walkFolder(${id},${folderPath}), adding child=${JSON.stringify(
|
||||||
|
// child
|
||||||
|
// )}`
|
||||||
|
// );
|
||||||
|
children.push(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
};
|
}
|
||||||
};
|
await queue.onIdle();
|
||||||
|
parents = children;
|
||||||
queue.add(newWalkTask(this.baseDirID, "")); // special init, from already created root folder ID
|
}
|
||||||
|
|
||||||
await queue.onIdle();
|
|
||||||
|
|
||||||
// console.debug(`in the end of walk:`);
|
// console.debug(`in the end of walk:`);
|
||||||
// console.debug(allFiles);
|
// console.debug(allFiles);
|
||||||
@ -325,97 +329,25 @@ export class FakeFsGoogleDrive extends FakeFs {
|
|||||||
return allFiles;
|
return allFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _listAllFiles(): Promise<GDEntity[]> {
|
async _walkFolder(parentID: string, parentFolderPath: string) {
|
||||||
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(
|
// console.debug(
|
||||||
// `input of single level: parentID=${parentID}, parentFolderPath=${parentFolderPath}`
|
// `input of single level: parentID=${parentID}, parentFolderPath=${parentFolderPath}`
|
||||||
// );
|
// );
|
||||||
const filesOneLevel: GDEntity[] = [];
|
const filesOneLevel: GDEntity[] = [];
|
||||||
let nextPageToken = "";
|
let nextPageToken: string | undefined = undefined;
|
||||||
if (parentID === undefined || parentID === "" || parentID === "root") {
|
if (parentID === undefined || parentID === "" || parentID === "root") {
|
||||||
// we should never start from root
|
// we should never start from root
|
||||||
// because we encapsulate the vault inside a folder
|
// because we encapsulate the vault inside a folder
|
||||||
throw Error(`something goes wrong walking folder`);
|
throw Error(`something goes wrong walking folder`);
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
const q = `'${parentID}' in parents and trashed=false`;
|
const q = encodeURIComponent(
|
||||||
const url = new URL("https://www.googleapis.com/drive/v3/files");
|
`'${parentID}' in parents and trashed=false`
|
||||||
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");
|
const pageToken =
|
||||||
url.searchParams.set("pageToken", nextPageToken);
|
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}`;
|
||||||
|
|
||||||
const k = await fetch(url, {
|
const k = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -445,7 +377,7 @@ export class FakeFsGoogleDrive extends FakeFs {
|
|||||||
|
|
||||||
async walkPartial(): Promise<Entity[]> {
|
async walkPartial(): Promise<Entity[]> {
|
||||||
await this._init();
|
await this._init();
|
||||||
const filesInLevel = await this._listFolder(this.baseDirID, "");
|
const filesInLevel = await this._walkFolder(this.baseDirID, "");
|
||||||
return filesInLevel;
|
return filesInLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,8 +508,6 @@ export class FakeFsGoogleDrive extends FakeFs {
|
|||||||
// "xxx" => []
|
// "xxx" => []
|
||||||
// "xxx/yyy/zzz.md" => ["xxx", "xxx/yyy"]
|
// "xxx/yyy/zzz.md" => ["xxx", "xxx/yyy"]
|
||||||
const folderLevels = getFolderLevels(key);
|
const folderLevels = getFolderLevels(key);
|
||||||
console.log(key);
|
|
||||||
console.log(folderLevels);
|
|
||||||
if (folderLevels.length === 0) {
|
if (folderLevels.length === 0) {
|
||||||
// root
|
// root
|
||||||
parentID = this.baseDirID;
|
parentID = this.baseDirID;
|
||||||
@ -592,42 +522,34 @@ export class FakeFsGoogleDrive extends FakeFs {
|
|||||||
parentID = this.keyToGDEntity[parentFolderPath].id;
|
parentID = this.keyToGDEntity[parentFolderPath].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetFileId = this.keyToGDEntity[key]?.id;
|
|
||||||
|
|
||||||
const fileItself = key.split("/").pop()!;
|
const fileItself = key.split("/").pop()!;
|
||||||
const meta: any = {
|
|
||||||
name: fileItself,
|
|
||||||
modifiedTime: unixTimeToStr(mtime, true),
|
|
||||||
createdTime: unixTimeToStr(ctime, true),
|
|
||||||
};
|
|
||||||
if (!targetFileId) meta.parents = [parentID];
|
|
||||||
|
|
||||||
if (content.byteLength <= 5 * 1024 * 1024) {
|
if (content.byteLength <= 5 * 1024 * 1024) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
const meta: any = {
|
||||||
|
name: fileItself,
|
||||||
|
modifiedTime: unixTimeToStr(mtime, true),
|
||||||
|
createdTime: unixTimeToStr(ctime, true),
|
||||||
|
parents: [parentID],
|
||||||
|
};
|
||||||
formData.append(
|
formData.append(
|
||||||
"metadata",
|
"metadata",
|
||||||
new Blob(targetFileId ? [] : [JSON.stringify(meta)], {
|
new Blob([JSON.stringify(meta)], {
|
||||||
type: "application/json; charset=UTF-8",
|
type: "application/json; charset=UTF-8",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
formData.append("media", new Blob([content], { type: contentType }));
|
formData.append("media", new Blob([content], { type: contentType }));
|
||||||
|
|
||||||
const url = new URL("https://www.googleapis.com/upload/drive/v3/files");
|
const res = await fetch(
|
||||||
if (targetFileId) url.pathname += `/${targetFileId}`;
|
"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",
|
||||||
url.searchParams.set("uploadType", "multipart");
|
{
|
||||||
url.searchParams.set(
|
method: "POST",
|
||||||
"fields",
|
headers: {
|
||||||
"kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum"
|
Authorization: `Bearer ${await this._getAccessToken()}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: targetFileId ? "PATCH" : "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${await this._getAccessToken()}`,
|
|
||||||
},
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
if (res.status !== 200 && res.status !== 201) {
|
if (res.status !== 200 && res.status !== 201) {
|
||||||
throw Error(`create file ${key} failed! meta=${JSON.stringify(meta)}`);
|
throw Error(`create file ${key} failed! meta=${JSON.stringify(meta)}`);
|
||||||
}
|
}
|
||||||
@ -642,7 +564,13 @@ export class FakeFsGoogleDrive extends FakeFs {
|
|||||||
this.keyToGDEntity[key] = entity;
|
this.keyToGDEntity[key] = entity;
|
||||||
return entity;
|
return entity;
|
||||||
} else {
|
} else {
|
||||||
const bodyStr = targetFileId ? "" : JSON.stringify(meta);
|
const meta: any = {
|
||||||
|
name: fileItself,
|
||||||
|
modifiedTime: unixTimeToStr(mtime, true),
|
||||||
|
createdTime: unixTimeToStr(ctime, true),
|
||||||
|
parents: [parentID],
|
||||||
|
};
|
||||||
|
const bodyStr = JSON.stringify(meta);
|
||||||
const headers: HeadersInit = {
|
const headers: HeadersInit = {
|
||||||
Authorization: `Bearer ${await this._getAccessToken()}`,
|
Authorization: `Bearer ${await this._getAccessToken()}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -650,18 +578,14 @@ export class FakeFsGoogleDrive extends FakeFs {
|
|||||||
"X-Upload-Content-Type": contentType,
|
"X-Upload-Content-Type": contentType,
|
||||||
"X-Upload-Content-Length": `${content.byteLength}`,
|
"X-Upload-Content-Length": `${content.byteLength}`,
|
||||||
};
|
};
|
||||||
const url = new URL("https://www.googleapis.com/upload/drive/v3/files");
|
const res = await fetch(
|
||||||
if (targetFileId) url.pathname += `/${targetFileId}`;
|
"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",
|
||||||
url.searchParams.set("uploadType", "resumable");
|
{
|
||||||
url.searchParams.set(
|
method: "POST",
|
||||||
"fields",
|
headers: headers,
|
||||||
"kind,fileExtension,md5Checksum,mimeType,parents,size,spaces,id,name,trashed,createdTime,modifiedTime,quotaBytesUsed,originalFilename,fullFileExtension,sha1Checksum,sha256Checksum"
|
body: bodyStr,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
const res = await fetch(url, {
|
|
||||||
method: targetFileId ? "PATCH" : "POST",
|
|
||||||
headers: headers,
|
|
||||||
body: bodyStr,
|
|
||||||
});
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw Error(
|
throw Error(
|
||||||
`create resumable file ${key} failed! meta=${JSON.stringify(
|
`create resumable file ${key} failed! meta=${JSON.stringify(
|
||||||
|
|||||||
@ -575,7 +575,7 @@ export class FakeFsOnedriveFull extends FakeFs {
|
|||||||
*/
|
*/
|
||||||
async _putUint8ArrayByRange(
|
async _putUint8ArrayByRange(
|
||||||
pathFragOrig: string,
|
pathFragOrig: string,
|
||||||
payload: Uint8Array<ArrayBuffer>,
|
payload: Uint8Array,
|
||||||
rangeStart: number,
|
rangeStart: number,
|
||||||
rangeEnd: number,
|
rangeEnd: number,
|
||||||
size: number
|
size: number
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
|
||||||
|
import AggregateError from "aggregate-error";
|
||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
import XRegExp from "xregexp";
|
import XRegExp from "xregexp";
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const getKeyIVFromPassword = async (
|
|||||||
const k2 = await window.crypto.subtle.deriveBits(
|
const k2 = await window.crypto.subtle.deriveBits(
|
||||||
{
|
{
|
||||||
name: "PBKDF2",
|
name: "PBKDF2",
|
||||||
salt: salt as Uint8Array<ArrayBuffer>,
|
salt: salt,
|
||||||
iterations: rounds,
|
iterations: rounds,
|
||||||
hash: "SHA-256",
|
hash: "SHA-256",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -729,7 +729,7 @@ export class FakeFsOnedrive extends FakeFs {
|
|||||||
*/
|
*/
|
||||||
async _putUint8ArrayByRange(
|
async _putUint8ArrayByRange(
|
||||||
pathFragOrig: string,
|
pathFragOrig: string,
|
||||||
payload: Uint8Array<ArrayBuffer>,
|
payload: Uint8Array,
|
||||||
rangeStart: number,
|
rangeStart: number,
|
||||||
rangeEnd: number,
|
rangeEnd: number,
|
||||||
size: number
|
size: number
|
||||||
|
|||||||
@ -22,6 +22,8 @@ import {
|
|||||||
import { requestTimeout } from "@smithy/fetch-http-handler/dist-es/request-timeout";
|
import { requestTimeout } from "@smithy/fetch-http-handler/dist-es/request-timeout";
|
||||||
import { type HttpRequest, HttpResponse } from "@smithy/protocol-http";
|
import { type HttpRequest, HttpResponse } from "@smithy/protocol-http";
|
||||||
import { buildQueryString } from "@smithy/querystring-builder";
|
import { buildQueryString } from "@smithy/querystring-builder";
|
||||||
|
// biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
|
||||||
|
import AggregateError from "aggregate-error";
|
||||||
import * as mime from "mime-types";
|
import * as mime from "mime-types";
|
||||||
import { Platform, type RequestUrlParam, requestUrl } from "obsidian";
|
import { Platform, type RequestUrlParam, requestUrl } from "obsidian";
|
||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
|
||||||
|
import AggregateError from "aggregate-error";
|
||||||
import cloneDeep from "lodash/cloneDeep";
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
import throttle from "lodash/throttle";
|
import throttle from "lodash/throttle";
|
||||||
import { FileText, RefreshCcw, RotateCcw, createElement } from "lucide";
|
import { FileText, RefreshCcw, RotateCcw, createElement } from "lucide";
|
||||||
|
|||||||
@ -88,12 +88,9 @@ export const mkdirpInVault = async (thePath: string, vault: Vault) => {
|
|||||||
* @returns ArrayBuffer
|
* @returns ArrayBuffer
|
||||||
*/
|
*/
|
||||||
export const bufferToArrayBuffer = (
|
export const bufferToArrayBuffer = (
|
||||||
b: Buffer | Uint8Array<ArrayBuffer> | ArrayBufferView
|
b: Buffer | Uint8Array | ArrayBufferView
|
||||||
) => {
|
) => {
|
||||||
return b.buffer.slice(
|
return b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
|
||||||
b.byteOffset,
|
|
||||||
b.byteOffset + b.byteLength
|
|
||||||
) as ArrayBuffer;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -8,13 +8,13 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "node",
|
||||||
// "allowSyntheticDefaultImports": true,
|
// "allowSyntheticDefaultImports": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"lib": ["dom", "es5", "scripthost", "es2015", "es2021", "webworker"]
|
"lib": ["dom", "es5", "scripthost", "es2015", "webworker"]
|
||||||
},
|
},
|
||||||
"include": ["src/global.d.ts", "**/*.ts"]
|
"include": ["src/global.d.ts", "**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,11 +65,6 @@ module.exports = {
|
|||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
process: "process/browser",
|
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: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user