mirror of
https://github.com/yv1ing/obsidian-cos-picbed.git
synced 2025-09-16 15:09:08 +08:00
完成粘贴上传和同步删除功能
This commit is contained in:
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal 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
|
||||||
35
README.md
35
README.md
@@ -1,2 +1,33 @@
|
|||||||
# obsidian-cos-picbed
|
# 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 存储桶,并能在需要删除图片时,同步删除存储桶中的图片文件,减少垃圾图片占用的存储空间。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
首先开通和设置 COS,参考官方教程或网上文章即可,这里不再赘述。然后到藤棍云控制台中获取 SecretId 和 SecretKey,并记下存储桶的相关信息:
|
||||||
|
|
||||||
|
1. `SecretId`
|
||||||
|
2. `SecretKey`
|
||||||
|
3. `Bucket`(存储桶名称)
|
||||||
|
4. `Region`(存储桶区域)
|
||||||
|
|
||||||
|
然后从 Github 仓库的 release 处下载插件,解压之后放到 `.obsidian/plugins` 目录下,重启 obsidian 再启用 `COS Picbed` 插件即可。插件配置如下:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
其中的 `Prefix` 指的是存储桶中的目录名称,图片将存储在这个目录下。
|
||||||
|
|
||||||
|
由于直接在插件中调用了 COS 的 SDK,所以会存在跨域问题,解决方案是在存储桶的安全设置中添加 CORS 规则:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app://obsidian.md
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
配置完成之后,直接粘贴图片即可自动上传,需要删除时,只需要右键点击图片,选择 “Delete this image” 即可。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|||||||
49
esbuild.config.mjs
Normal file
49
esbuild.config.mjs
Normal 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
345
main.ts
Normal 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(``, 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
12
manifest.json
Normal 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
3262
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal 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
25
tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user