diff --git a/apps/joplin-vscode-plugin/package.json b/apps/joplin-vscode-plugin/package.json index 151b7c38..acd9d93b 100644 --- a/apps/joplin-vscode-plugin/package.json +++ b/apps/joplin-vscode-plugin/package.json @@ -41,15 +41,18 @@ "@types/koa__cors": "^3.3.0", "@types/koa__router": "^12.0.0", "@types/markdown-it": "^12.2.3", + "@types/mime-types": "^2.1.1", "@types/node": "^16", "@types/vscode": "^1.71.0", "chokidar": "^3.5.3", "cross-path-sort": "^1.0.0", + "envfile": "^6.18.0", "fetch-blob": "^3.2.0", "formdata-polyfill": "^4.0.10", "joplin-api": "workspace:*", "koa": "^2.13.4", "markdown-it": "^13.0.1", + "mime-types": "^2.1.35", "node-fetch": "^3.2.10", "node-html-parser": "^6.1.1", "tsup": "^6.2.3", diff --git a/apps/joplin-vscode-plugin/src/service/JoplinNoteCommandService.ts b/apps/joplin-vscode-plugin/src/service/JoplinNoteCommandService.ts index 630d42ae..ae1801ca 100644 --- a/apps/joplin-vscode-plugin/src/service/JoplinNoteCommandService.ts +++ b/apps/joplin-vscode-plugin/src/service/JoplinNoteCommandService.ts @@ -17,7 +17,7 @@ import { FolderOrNoteExtendsApi } from '../api/FolderOrNoteExtendsApi' import { appConfig, AppConfig } from '../config/AppConfig' import { JoplinNoteUtil } from '../util/JoplinNoteUtil' import * as path from 'path' -import { close, createReadStream, mkdirp, pathExists, readFile, remove, writeFile } from '@liuli-util/fs-extra' +import { close, mkdirp, pathExists, readFile, remove, writeFile } from '@liuli-util/fs-extra' import { createEmptyFile } from '../util/createEmptyFile' import { UploadResourceUtil } from '../util/UploadResourceUtil' import { uploadResourceService } from './UploadResourceService' @@ -34,7 +34,6 @@ import { filenamify } from '../util/filenamify' import { NoteProperties } from 'joplin-api' import { logger } from '../constants/logger' import { loadLastNoteList } from '../util/api' -import { Blob } from 'buffer' export class JoplinNoteCommandService { private folderOrNoteExtendsApi = new FolderOrNoteExtendsApi() @@ -89,11 +88,18 @@ export class JoplinNoteCommandService { if (!id) { return } - await resourceApi.update({ - id, - // title: path.basename(filePath), - data: new Blob([await readFile(filePath)]), - }) + try { + const data = await readFile(filePath) + const r = await resourceApi.update({ + id, + // @ts-expect-error + data: new Blob([data]), + filename: path.basename(filePath), + }) + console.log('resource update? ', r) + } catch (err) { + logger.error('update resource error: ' + err) + } }) .on('error', (err) => { logger.error('watch resource error: ' + err) diff --git a/apps/joplin-vscode-plugin/src/util/UploadResourceUtil.ts b/apps/joplin-vscode-plugin/src/util/UploadResourceUtil.ts index 6852f83f..31f13a57 100644 --- a/apps/joplin-vscode-plugin/src/util/UploadResourceUtil.ts +++ b/apps/joplin-vscode-plugin/src/util/UploadResourceUtil.ts @@ -4,7 +4,6 @@ import { existsSync, mkdirpSync, readFile } from '@liuli-util/fs-extra' import { spawn } from 'child_process' import { resourceApi } from 'joplin-api' import { RootPath } from '../RootPath' -import { Blob } from 'buffer' /** * for clipboard image @@ -16,29 +15,20 @@ export interface IClipboardImage { export class UploadResourceUtil { static async uploadByPath(filePath: string, isImage: boolean) { - const param = { - title: path.basename(filePath), + const title = path.basename(filePath) + console.log('uploadFromExplorer begin: ', filePath, title) + const res = await resourceApi.create({ + title, + // @ts-expect-error data: new Blob([await readFile(filePath)]), - } - console.log('uploadFromExplorer begin: ', filePath, param.title) - const res = await resourceApi.create(param) - const markdownLink = `${isImage ? '!' : ''}[${param.title}](:/${res.id})` + filename: title, + }) + console.log('uploadByPath', res) + const markdownLink = `${isImage ? '!' : ''}[${title}](:/${res.id})` console.log('uploadFromExplorer end: ', markdownLink) return { res, markdownLink } } - static async uploadFileByPath(filePath: string) { - const param = { - title: path.basename(filePath), - data: new Blob([await readFile(filePath)]), - } - console.log('uploadFileFromExplorer begin: ', filePath, param.title) - const res = await resourceApi.create(param) - const markdownLink = `[${res.title}](:/${res.id})` - console.log('uploadFileFromExplorer end: ', markdownLink) - return { res, markdownLink } - } - static getCurrentPlatform(): string { const platform = process.platform if (platform !== 'win32') { diff --git a/apps/joplin-vscode-plugin/src/util/__tests__/UploadResourceUtil.test.ts b/apps/joplin-vscode-plugin/src/util/__tests__/UploadResourceUtil.test.ts new file mode 100644 index 00000000..8c2b6ce8 --- /dev/null +++ b/apps/joplin-vscode-plugin/src/util/__tests__/UploadResourceUtil.test.ts @@ -0,0 +1,35 @@ +import { close, mkdirp, pathExists, readFile, remove } from '@liuli-util/fs-extra' +import { config, resourceApi } from 'joplin-api' +import path from 'path' +import { beforeEach, expect, it } from 'vitest' +import { findParent } from '../findParent' +import { UploadResourceUtil } from '../UploadResourceUtil' +import { parse } from 'envfile' +import { createEmptyFile } from '../createEmptyFile' +import '../node-polyfill' + +const tempPath = path.resolve(__dirname, '.temp', path.basename(__filename)) +beforeEach(async () => { + await remove(tempPath) + await mkdirp(tempPath) + const dirPath = await findParent(__dirname, (item) => pathExists(path.resolve(item, 'package.json'))) + const envPath = path.resolve(dirPath!, '.env.local') + if (!(await pathExists(envPath))) { + throw new Error('请更新 .env.local 文件:' + envPath) + } + const env = await readFile(envPath, 'utf8') + + config.token = parse(env).TOKEN! + config.baseUrl = 'http://127.0.0.1:27583' +}) + +it('test create by empty file', async () => { + const fsPath = path.resolve(__dirname, 'assets/test.km.svg') + const { res, markdownLink } = await UploadResourceUtil.uploadByPath(fsPath, true) + expect(res.mime).eq('image/svg+xml') + expect(markdownLink).eq(`![${path.basename(fsPath)}](:/${res.id})`) + const data = await readFile(fsPath) + const buffer = await resourceApi.fileByResourceId(res.id) + expect(data).deep.eq(buffer) + console.log(data.length) +}) diff --git a/apps/joplin-vscode-plugin/src/util/__tests__/assets/test.km.svg b/apps/joplin-vscode-plugin/src/util/__tests__/assets/test.km.svg new file mode 100644 index 00000000..ae970274 --- /dev/null +++ b/apps/joplin-vscode-plugin/src/util/__tests__/assets/test.km.svg @@ -0,0 +1 @@ +testtopic \ No newline at end of file diff --git a/apps/joplin-vscode-plugin/src/util/findParent.ts b/apps/joplin-vscode-plugin/src/util/findParent.ts new file mode 100644 index 00000000..b92aabdb --- /dev/null +++ b/apps/joplin-vscode-plugin/src/util/findParent.ts @@ -0,0 +1,27 @@ +import path from 'path' + +/** + * 向上查找目录 + * @param cwd + * @param predicate + */ +export function findParent(cwd: string, predicate: (dir: string) => boolean): string | null +export function findParent(cwd: string, predicate: (dir: string) => Promise): Promise +export function findParent boolean | Promise, R extends string | null>( + cwd: string, + predicate: T, +): ReturnType extends Promise ? Promise : R { + const res = predicate(cwd) + function f(res: boolean): string | null { + if (res) { + return cwd + } + const parent = path.dirname(cwd) + if (parent === cwd) { + return null + } + return findParent(parent, predicate as any) + } + + return res instanceof Promise ? res.then(f) : (f(res) as any) +} diff --git a/apps/joplin-vscode-plugin/src/util/node-polyfill.ts b/apps/joplin-vscode-plugin/src/util/node-polyfill.ts index 466847c1..ad083a79 100644 --- a/apps/joplin-vscode-plugin/src/util/node-polyfill.ts +++ b/apps/joplin-vscode-plugin/src/util/node-polyfill.ts @@ -1,10 +1,7 @@ -import nodeFetch from 'node-fetch' +import fetch from 'node-fetch' import { FormData } from 'formdata-polyfill/esm.min.js' +import { Blob } from 'fetch-blob' -// @ts-expect-errors -if (typeof fetch === 'undefined') { - Reflect.set(globalThis, 'fetch', nodeFetch) -} -if (typeof FormData === 'undefined') { - Reflect.set(globalThis, 'FormData', FormData) -} +Reflect.set(globalThis, 'fetch', fetch) +Reflect.set(globalThis, 'FormData', FormData) +Reflect.set(globalThis, 'Blob', Blob) diff --git a/libs/joplin-api/package.json b/libs/joplin-api/package.json index 3276e100..0b4d4fe3 100644 --- a/libs/joplin-api/package.json +++ b/libs/joplin-api/package.json @@ -34,8 +34,12 @@ "devDependencies": { "@liuli-util/eslint-config-ts": "^0.4.0", "@types/fs-extra": "^9.0.13", + "@types/node": "^18.11.3", "envfile": "^6.18.0", + "fetch-blob": "^3.2.0", + "formdata-polyfill": "^4.0.10", "fs-extra": "^10.1.0", + "node-fetch": "^3.2.10", "rimraf": "^3.0.2", "ts-node": "^10.9.1", "tslib": "^2.4.0", diff --git a/libs/joplin-api/src/api/ResourceApi.ts b/libs/joplin-api/src/api/ResourceApi.ts index 43821b05..19799d91 100644 --- a/libs/joplin-api/src/api/ResourceApi.ts +++ b/libs/joplin-api/src/api/ResourceApi.ts @@ -42,12 +42,13 @@ export class ResourceApi { * The "data" field is required, while the "props" one is not. If not specified, default values will be used. * @param param */ - async create(param: { data: Blob | BufferBlob } & Partial): Promise { + async create(param: { data: Blob | BufferBlob } & Partial): Promise { const { data, ...others } = param return (await this.ajax.postFormData('/resources', 'post', { props: JSON.stringify(others), - data: param.data, - })) as ResourceGetRes + data: data, + filename: param.filename, + })) as ResourceProperties } async update( @@ -57,6 +58,7 @@ export class ResourceApi { return await this.ajax.postFormData(`/resources/${id}`, 'put', { props: JSON.stringify(others), data: data, + filename: param.filename, }) } diff --git a/libs/joplin-api/src/api/__tests__/ResourceApi.test.ts b/libs/joplin-api/src/api/__tests__/ResourceApi.test.ts index 94bf391e..6892e809 100644 --- a/libs/joplin-api/src/api/__tests__/ResourceApi.test.ts +++ b/libs/joplin-api/src/api/__tests__/ResourceApi.test.ts @@ -1,6 +1,6 @@ import { expect, it, describe, beforeAll, afterAll, beforeEach } from 'vitest' import { resourceApi } from '../..' -import { mkdirp, pathExists, readFile, remove, stat, writeFile } from 'fs-extra' +import { mkdirp, pathExists, readFile, remove, stat, writeFile, open, close } from 'fs-extra' import { createTestResource } from './utils/CreateTestResource' import path from 'path' import { setupTestEnv } from '../../util/setupTestEnv' @@ -40,12 +40,29 @@ it.skip('test get filename', async () => { const resourcePath = path.resolve(__dirname, './assets/resourcesByFileId.png') it('test create', async () => { const title = 'image title' - const json = await resourceApi.create({ + const r = await resourceApi.create({ title, data: new Blob([await readFile(resourcePath)]), }) - console.log(json.id) - expect(json.title).toBe(title) + console.log(r) + expect(r.title).toBe(title) +}) +it.only('test create empty file', async () => { + const fsPath = path.resolve(tempPath, 'test.km.svg') + const handle = await open(fsPath, 'w') + try { + expect(await pathExists(fsPath)).toBeTruthy() + const r = await resourceApi.create({ + title: path.basename(fsPath), + data: new Blob([await readFile(fsPath)]), + filename: path.basename(fsPath), + }) + console.log(r) + expect(r.mime).eq('image/svg+xml') + expect(r.file_extension).eq('svg') + } finally { + await close(handle) + } }) it('create by buffer', async () => { const title = 'image title' diff --git a/libs/joplin-api/src/api/__tests__/assets/test.km.svg b/libs/joplin-api/src/api/__tests__/assets/test.km.svg new file mode 100644 index 00000000..3f078daf --- /dev/null +++ b/libs/joplin-api/src/api/__tests__/assets/test.km.svg @@ -0,0 +1 @@ +MainTopic \ No newline at end of file diff --git a/libs/joplin-api/src/api/__tests__/fixs/ResourceApiCreateByPolyfill.test.ts b/libs/joplin-api/src/api/__tests__/fixs/ResourceApiCreateByPolyfill.test.ts new file mode 100644 index 00000000..ef8c645b --- /dev/null +++ b/libs/joplin-api/src/api/__tests__/fixs/ResourceApiCreateByPolyfill.test.ts @@ -0,0 +1,61 @@ +import { mkdirp, pathExists, readFile, remove, open, close, writeFile } from 'fs-extra' +import path from 'path' +import { beforeEach, expect, it } from 'vitest' +import { resourceApi } from '../../JoplinApiGenerator' +import fetch from 'node-fetch' +import { FormData } from 'formdata-polyfill/esm.min.js' +import { Blob } from 'fetch-blob' + +const tempPath = path.resolve(__dirname, '.temp') +beforeEach(async () => { + await remove(tempPath) + await mkdirp(tempPath) + Reflect.set(globalThis, 'fetch', fetch) + Reflect.set(globalThis, 'FormData', FormData) + Reflect.set(globalThis, 'Blob', Blob) +}) + +it('create empty file and polyfill', async () => { + const emptyPath = path.resolve(tempPath, 'test.km.svg') + const handle = await open(emptyPath, 'w') + try { + expect(await pathExists(emptyPath)).toBeTruthy() + const r = await resourceApi.create({ + title: path.basename(emptyPath), + data: new Blob([await readFile(emptyPath)]), + }) + expect(r).not.undefined + } finally { + await close(handle) + } +}) + +it('create file and polyfill', async () => { + const fsPath = path.resolve(__dirname, '../assets/resourcesByFileId.png') + const data = await readFile(fsPath) + const r = await resourceApi.create({ + title: path.basename(fsPath), + data: new Blob([data]), + }) + expect(r).not.undefined + expect(data).deep.eq(await resourceApi.fileByResourceId(r.id)) +}) + +it('update file', async () => { + const emptyPath = path.resolve(tempPath, 'test.km.svg') + const fsPath = path.resolve(__dirname, '../assets/resourcesByFileId.png') + const handle = await open(emptyPath, 'w') + try { + expect(await pathExists(emptyPath)).toBeTruthy() + const r = await resourceApi.create({ + title: path.basename(emptyPath), + data: new Blob([await readFile(emptyPath)]), + }) + expect(r).not.undefined + const data = await readFile(fsPath) + await resourceApi.update({ id: r.id, data: new Blob([data]) }) + expect(data).deep.eq(await resourceApi.fileByResourceId(r.id)) + } finally { + await close(handle) + } +}) diff --git a/libs/joplin-api/src/util/ajax.ts b/libs/joplin-api/src/util/ajax.ts index a15883c0..2a559caf 100644 --- a/libs/joplin-api/src/util/ajax.ts +++ b/libs/joplin-api/src/util/ajax.ts @@ -104,13 +104,20 @@ export class Ajax { }) } - async postFormData(url: string, method: 'post' | 'put', data: object): Promise { + async postFormData( + url: string, + method: 'post' | 'put', + data: { + props: string + data?: Blob + filename?: string + }, + ): Promise { const fd = new FormData() - Object.entries(data).forEach(([k, v]) => { - if (k && v) { - fd.append(k, v) - } - }) + fd.append('props', data.props) + if (data.data) { + fd.append('data', data.data, data.filename) + } return await this.request({ url: this.baseUrl(url), method: method, data: fd }) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8cb7b0fb..6c4bdb1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,15 +206,18 @@ importers: '@types/koa__cors': ^3.3.0 '@types/koa__router': ^12.0.0 '@types/markdown-it': ^12.2.3 + '@types/mime-types': ^2.1.1 '@types/node': ^16 '@types/vscode': ^1.71.0 chokidar: ^3.5.3 cross-path-sort: ^1.0.0 + envfile: ^6.18.0 fetch-blob: ^3.2.0 formdata-polyfill: ^4.0.10 joplin-api: workspace:* koa: ^2.13.4 markdown-it: ^13.0.1 + mime-types: ^2.1.35 node-fetch: ^3.2.10 node-html-parser: ^6.1.1 tsup: ^6.2.3 @@ -235,15 +238,18 @@ importers: '@types/koa__cors': 3.3.0 '@types/koa__router': 12.0.0 '@types/markdown-it': 12.2.3 + '@types/mime-types': 2.1.1 '@types/node': 16.11.68 '@types/vscode': 1.71.0 chokidar: 3.5.3 cross-path-sort: 1.0.0 + envfile: 6.18.0 fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 joplin-api: link:../../libs/joplin-api koa: 2.13.4 markdown-it: 13.0.1 + mime-types: 2.1.35 node-fetch: 3.2.10 node-html-parser: 6.1.1 tsup: 6.2.3_typescript@4.8.4 @@ -366,8 +372,12 @@ importers: '@liuli-util/eslint-config-ts': ^0.4.0 '@liuli-util/object': ^3.5.0 '@types/fs-extra': ^9.0.13 + '@types/node': ^18.11.3 envfile: ^6.18.0 + fetch-blob: ^3.2.0 + formdata-polyfill: ^4.0.10 fs-extra: ^10.1.0 + node-fetch: ^3.2.10 query-string: ^7.1.1 rimraf: ^3.0.2 ts-node: ^10.9.1 @@ -382,10 +392,14 @@ importers: devDependencies: '@liuli-util/eslint-config-ts': 0.4.0_typescript@4.8.4 '@types/fs-extra': 9.0.13 + '@types/node': 18.11.3 envfile: 6.18.0 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 fs-extra: 10.1.0 + node-fetch: 3.2.10 rimraf: 3.0.2 - ts-node: 10.9.1_typescript@4.8.4 + ts-node: 10.9.1_lw7q66ikwuedwcorwkk4v6trsa tslib: 2.4.0 tsup: 6.2.3_mwhvu7sfp6vq5ryuwb6hlbjfka typedoc: 0.23.15_typescript@4.8.4 @@ -3356,7 +3370,7 @@ packages: /@types/fs-extra/9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 18.7.23 + '@types/node': 18.11.3 /@types/gh-pages/3.2.1: resolution: {integrity: sha512-y5ULkwfoOEUa6sp2te+iEODv2S//DRiKmxpeXboXhhv+s758rSSxLUiBd6NnlR7aAY4nw1X4FGovLrSWEXWLow==} @@ -3513,6 +3527,10 @@ packages: resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==} dev: true + /@types/mime-types/2.1.1: + resolution: {integrity: sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==} + dev: true + /@types/mime/3.0.1: resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} dev: true @@ -3536,8 +3554,12 @@ packages: resolution: {integrity: sha512-JkRpuVz3xCNCWaeQ5EHLR/6woMbHZz/jZ7Kmc63AkU+1HxnoUugzSWMck7dsR4DvNYX8jp9wTi9K7WvnxOIQZQ==} dev: true + /@types/node/18.11.3: + resolution: {integrity: sha512-fNjDQzzOsZeKZu5NATgXUPsaFaTxeRgFXoosrHivTl8RGeV733OLawXsGfEk9a8/tySyZUyiZ6E8LcjPFZ2y1A==} + /@types/node/18.7.23: resolution: {integrity: sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg==} + dev: true /@types/normalize-package-data/2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -13512,7 +13534,7 @@ packages: optional: true dependencies: lilconfig: 2.0.6 - ts-node: 10.9.1_typescript@4.8.4 + ts-node: 10.9.1_lw7q66ikwuedwcorwkk4v6trsa yaml: 1.10.2 dev: true @@ -16544,7 +16566,7 @@ packages: yn: 3.1.1 dev: true - /ts-node/10.9.1_typescript@4.8.4: + /ts-node/10.9.1_lw7q66ikwuedwcorwkk4v6trsa: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -16563,6 +16585,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 + '@types/node': 18.11.3 acorn: 8.8.0 acorn-walk: 8.2.0 arg: 4.1.3 @@ -17364,7 +17387,7 @@ packages: dependencies: '@types/chai': 4.3.3 '@types/chai-subset': 1.3.3 - '@types/node': 18.7.23 + '@types/node': 18.11.3 chai: 4.3.6 debug: 4.3.4 local-pkg: 0.4.2