完成粘贴上传和同步删除功能

This commit is contained in:
2025-04-07 00:44:50 +08:00
parent b4997068ba
commit 09c765311d
8 changed files with 3775 additions and 2 deletions

22
.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store

View File

@@ -1,2 +1,33 @@
# obsidian-cos-picbed
This plug-in is used to automatically upload pictures to Tencent Cloud COS, and supports manual deletion of useless pictures to reduce storage space!
# Obsidian COS Picbed
这是一个 obsidian 插件,用于自动将图片自动上传到 COS 存储桶,并能在需要删除图片时,同步删除存储桶中的图片文件,减少垃圾图片占用的存储空间。
## 使用说明
首先开通和设置 COS参考官方教程或网上文章即可这里不再赘述。然后到藤棍云控制台中获取 SecretId 和 SecretKey并记下存储桶的相关信息
1. `SecretId`
2. `SecretKey`
3. `Bucket`(存储桶名称)
4. `Region`(存储桶区域)
然后从 Github 仓库的 release 处下载插件,解压之后放到 `.obsidian/plugins` 目录下,重启 obsidian 再启用 `COS Picbed` 插件即可。插件配置如下:
![image.png](https://yvling-images-1257337367.cos.ap-nanjing.myqcloud.com/0/1743956661826.png)
其中的 `Prefix` 指的是存储桶中的目录名称,图片将存储在这个目录下。
由于直接在插件中调用了 COS 的 SDK所以会存在跨域问题解决方案是在存储桶的安全设置中添加 CORS 规则:
```text
app://obsidian.md
```
![image.png](https://yvling-images-1257337367.cos.ap-nanjing.myqcloud.com/0/1743956601246.png)
配置完成之后,直接粘贴图片即可自动上传,需要删除时,只需要右键点击图片,选择 “Delete this image” 即可。
![image.png](https://yvling-images-1257337367.cos.ap-nanjing.myqcloud.com/0/1743957687756.png)

49
esbuild.config.mjs Normal file
View File

@@ -0,0 +1,49 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

345
main.ts Normal file
View File

@@ -0,0 +1,345 @@
import COS from "cos-js-sdk-v5";
import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, TFile, } from "obsidian";
interface CosPicbedPluginSettings {
secretId: string;
secretKey: string;
bucket: string;
region: string;
prefix: string;
}
class CosUploader {
private cos: any;
private settings: CosPicbedPluginSettings;
private updateInterval: NodeJS.Timeout | null = null;
constructor(settings: CosPicbedPluginSettings) {
this.settings = settings;
if (!settings.secretId || !settings.secretKey) {
throw new Error("SecretId and SecretKey are empty!");
}
this.cos = new COS({
Protocol: "https:",
SecretId: settings.secretId,
SecretKey: settings.secretKey,
});
}
public cleanup() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
this.updateInterval = null;
}
}
// 上传文件
async uploadFile(file: File): Promise<string> {
if (!this.settings.bucket || !this.settings.region) {
throw new Error("Bucket and Region are empty!");
}
// 修改文件名
const originalName = file.name;
const extension = originalName.split(".").pop();
const fileName = `${Date.now()}.${extension}`;
const prefix = this.settings.prefix ? `${this.settings.prefix}/` : "";
const fullPath = `${prefix}${fileName}`;
return new Promise((resolve, reject) => {
this.cos.putObject(
{
Bucket: this.settings.bucket,
Region: this.settings.region,
Key: fullPath,
Body: file,
},
async (err: any, data: any) => {
if (err) {
reject(err);
return;
}
try {
const url = await this.getUrl(fullPath);
resolve(url);
} catch (error) {
reject(error);
return;
}
}
);
});
}
// 删除文件
async deleteFile(fileName: string): Promise<string> {
const prefix = this.settings.prefix ? `${this.settings.prefix}/` : "";
const fullPath = `${prefix}${fileName}`;
return new Promise((resolve, reject) => {
this.cos.deleteObject(
{
Bucket: this.settings.bucket,
Region: this.settings.region,
Key: fullPath,
},
(err: any, data: any) => {
if (err) {
reject(err);
return;
}
resolve(data.Url);
});
});
}
// 获取图片URL
private getUrl(fileName: string): Promise<string> {
return new Promise((resolve, reject) => {
this.cos.getObjectUrl(
{
Bucket: this.settings.bucket,
Region: this.settings.region,
Key: fileName,
Sign: false,
},
(err: any, data: any) => {
if (err) {
reject(err);
return;
}
resolve(data.Url);
}
);
});
}
}
class CosPicbedSettingTab extends PluginSettingTab {
plugin: CosPicbedPlugin;
private initUploader: () => void;
constructor(app: App, plugin: CosPicbedPlugin, initUploader: () => void) {
super(app, plugin);
this.plugin = plugin;
this.initUploader = initUploader;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName("Secret Id")
.addText((text) =>
text
.setValue(this.plugin.settings.secretId)
.onChange(async (value) => {
this.plugin.settings.secretId = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Secret Key")
.addText((text) =>
text
.setValue(this.plugin.settings.secretKey)
.onChange(async (value) => {
this.plugin.settings.secretKey = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Bucket")
.addText((text) =>
text
.setPlaceholder("example-1250000000")
.setValue(this.plugin.settings.bucket)
.onChange(async (value) => {
this.plugin.settings.bucket = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Region")
.addDropdown((dropdown) => {
dropdown
.addOption("ap-beijing", "Beijing")
.addOption("ap-chengdu", "Chengdu")
.addOption("ap-nanjing", "Nanjing")
.addOption("ap-shanghai", "Shanghai")
.addOption("ap-hongkong", "Hongkong")
.addOption("ap-guangzhou", "Guangzhou")
.addOption("ap-chongqing", "Chongqing")
.setValue(this.plugin.settings.region)
.onChange(async (value) => {
this.plugin.settings.region = value.trim();
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Prefix")
.addText((text) =>
text
.setValue(this.plugin.settings.prefix)
.onChange(async (value) => {
let prefix = value.trim().replace(/^\/+|\/+$/g, "");
this.plugin.settings.prefix = prefix;
await this.plugin.saveSettings();
})
);
}
}
export default class CosPicbedPlugin extends Plugin {
settings: CosPicbedPluginSettings;
private uploader: CosUploader;
onunload() {
if (this.uploader) {
this.uploader.cleanup();
}
new Notice("COS Picbed has been uninstalled!");
}
async onload() {
await this.loadSettings();
// 初始化COS
if (!this.settings.secretId || !this.settings.secretKey || !this.settings.bucket || !this.settings.region) {
new Notice("COS configuration is empty!");
} else {
this.uploader = new CosUploader(this.settings);
}
new Notice("COS Picbed is loaded!");
// 粘贴图片上传
this.registerEvent(
this.app.workspace.on("editor-paste", async (evt: ClipboardEvent, editor: Editor, markdownView: MarkdownView) => {
const files = evt.clipboardData?.files;
if (!files || files.length === 0) {
return;
}
for (let i = 0; i < files.length; i++) {
const file = files[i];
evt.preventDefault();
try {
const activeFile = markdownView.file;
if (!activeFile) {
continue;
}
const url = await this.uploader.uploadFile(file);
const pos = editor.getCursor();
editor.replaceRange(`![${file.name}](${url})`, pos);
await new Promise((resolve) => setTimeout(resolve, 100));
await this.app.vault.process(activeFile, (content) => {
const imageRegex = /!\[\[(.*?)\]\]/g;
const matches = [...content.matchAll(imageRegex)];
for (const match of matches) {
const imagePath = match[1];
const imageFile = this.findImageFile(imagePath, activeFile);
if (imageFile instanceof TFile) {
this.app.fileManager.trashFile(imageFile);
content = content.replace(`![[${imagePath}]]`, "");
}
}
return content;
});
new Notice("Image upload successfully!");
} catch (error) {
new Notice("Image upload failed:" + error.message);
}
}
}
)
);
// 右键菜单删除
this.registerEvent(
this.app.workspace.on("editor-menu", (menu, editor) => {
const cursor = editor.getCursor();
const lineContent = editor.getLine(cursor.line);
const imageMatch = lineContent.match(/!\[.*?\]\((.*?)\)/);
if (imageMatch) {
menu.addItem((item) => {
item.setTitle("Delete this image")
.setIcon("trash")
.onClick(async () => {
const imageName = imageMatch[1].split('/').pop()!;
try {
this.uploader.deleteFile(imageName);
const newContent = lineContent.replace(imageMatch[0], '');
editor.setLine(cursor.line, newContent);
new Notice("Image delete successfully!");
} catch (error) {
new Notice("Image delete failed:" + error.message);
}
});
});
}
})
);
this.addSettingTab(new CosPicbedSettingTab(this.app, this, () => { }));
this.registerInterval(window.setInterval(() => { }, 5 * 60 * 1000));
}
async loadSettings() {
this.settings = Object.assign(await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
// 查找图片文件
private findImageFile(imagePath: string, currentFile: TFile): TFile | null {
let imageFile = this.app.vault.getAbstractFileByPath(imagePath);
if (imageFile instanceof TFile && this.isImageFile(imageFile)) {
return imageFile;
}
if (currentFile.parent) {
const relativePath = `${currentFile.parent.path}/${imagePath}`;
imageFile = this.app.vault.getAbstractFileByPath(relativePath);
if (imageFile instanceof TFile && this.isImageFile(imageFile)) {
return imageFile;
}
}
imageFile = this.app.vault.getAbstractFileByPath(`/${imagePath}`);
if (imageFile instanceof TFile && this.isImageFile(imageFile)) {
return imageFile;
}
const files = this.app.vault.getFiles();
return (
files.find((file) => {
file.name === imagePath && this.isImageFile(file)
}) || null
);
}
private isImageFile(file: TFile): boolean {
return (file.extension.toLowerCase().match(/png|jpg|jpeg|gif|svg|webp/i) !== null);
}
}

12
manifest.json Normal file
View File

@@ -0,0 +1,12 @@
{
"id": "obsidian-cos-picbed",
"name": "COS Picbed",
"version": "1.0.0",
"minAppVersion": "1.0.0",
"description": "This plug-in is used to automatically upload pictures to Tencent Cloud COS, and supports manual deletion of useless pictures to reduce storage space!",
"author": "yv1ing",
"authorUrl": "https://github.com/yv1ing",
"fundingUrl": "",
"helpUrl": "",
"isDesktopOnly": false
}

3262
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "obsidian-cos-picbed",
"version": "1.0.0",
"description": "This plug-in is used to automatically upload pictures to Tencent Cloud COS, and supports manual deletion of useless pictures to reduce storage space!",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
},
"keywords": ["obsidian", "cos", "picbed"],
"author": "yv1ing",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"tslib": "^2.4.0",
"esbuild": "^0.17.3",
"obsidian": "^1.8.7",
"typescript": "^4.7.4",
"builtin-modules": "^3.3.0",
"cos-nodejs-sdk-v5": "^2.14.4"
},
"dependencies": {
"cos-js-sdk-v5": "^1.8.7"
}
}

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"allowSyntheticDefaultImports": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
]
}