Compare commits

..

No commits in common. "39711d117cd417a708f086848092be05dd310d1d" and "34db181af002f8d71ea0a87e7965abc57b294914" have entirely different histories.

25 changed files with 136 additions and 11988 deletions

View File

@ -1,6 +0,0 @@
{
"all": true,
"include": ["src/**/*.ts", "pro/src/**/*.ts"],
"exclude": ["tests/**", "pro/tests/**", "**/*.d.ts", "**/langs/**"],
"reports-dir": "coverage"
}

View File

@ -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
View File

@ -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

View File

@ -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.

View File

@ -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,

View File

@ -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.

View File

@ -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 420 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`.

View File

@ -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
```

View File

@ -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,

View File

@ -1,2 +0,0 @@
[tools]
node = "24.15.0"

11423
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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`, {

View File

@ -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 {

View File

@ -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 = {

View File

@ -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
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);
for (const f of filesUnderFolder) { for (const f of filesUnderFolder) {
allFiles.push(f); allFiles.push(f);
if (f.isFolder) { if (f.isFolder) {
// keyRaw itself already has a tailing slash, no more slash here // keyRaw itself already has a tailing slash, no more slash here
// keyRaw itself also already has full path // keyRaw itself also already has full path
queue.add(newWalkTask(f.id, f.keyRaw)); const child = {
id: f.id,
folderPath: 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(); await queue.onIdle();
parents = children;
}
// 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()!;
if (content.byteLength <= 5 * 1024 * 1024) {
const formData = new FormData();
const meta: any = { const meta: any = {
name: fileItself, name: fileItself,
modifiedTime: unixTimeToStr(mtime, true), modifiedTime: unixTimeToStr(mtime, true),
createdTime: unixTimeToStr(ctime, true), createdTime: unixTimeToStr(ctime, true),
parents: [parentID],
}; };
if (!targetFileId) meta.parents = [parentID];
if (content.byteLength <= 5 * 1024 * 1024) {
const formData = new FormData();
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",
"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: {
Authorization: `Bearer ${await this._getAccessToken()}`, Authorization: `Bearer ${await this._getAccessToken()}`,
}, },
body: formData, 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",
"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, headers: headers,
body: bodyStr, 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(

View File

@ -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

View File

@ -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 {

View File

@ -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",
}, },

View File

@ -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

View File

@ -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";

View File

@ -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";

View File

@ -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;
}; };
/** /**

View File

@ -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"]
} }

View File

@ -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: [