mirror of
https://github.com/yv1ing/obsidian-cos-picbed.git
synced 2025-09-16 15:09:08 +08:00
389 lines
9.6 KiB
TypeScript
389 lines
9.6 KiB
TypeScript
import { error } from "console";
|
|
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;
|
|
}
|
|
|
|
const DEFAULT_SETTINGS: CosPicbedPluginSettings = {
|
|
secretId: "",
|
|
secretKey: "",
|
|
bucket: "",
|
|
region: "",
|
|
prefix: "",
|
|
};
|
|
|
|
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
|
|
.setPlaceholder("xxxxxxxxxxxxxxxxxxxxxx")
|
|
.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
|
|
.setPlaceholder("xxxxxxxxxxxxxxxxxxxxxx")
|
|
.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")
|
|
.addText((text) =>
|
|
text
|
|
.setPlaceholder("ap-beijing")
|
|
.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
|
|
.setPlaceholder("/")
|
|
.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.registerEvent(
|
|
this.app.workspace.on("editor-menu", (menu, editor) => {
|
|
const self = this;
|
|
|
|
menu.addItem((item) => {
|
|
item.setTitle("Delete all images")
|
|
.setIcon("trash")
|
|
.onClick(async () => {
|
|
const allContent = editor.getValue();
|
|
const imageMatches = [...allContent.matchAll(/!\[.*?\]\((.*?)\)/g)];
|
|
|
|
if (imageMatches.length == 0) {
|
|
new Notice("No image found!");
|
|
return;
|
|
}
|
|
|
|
const imageNames = imageMatches.map(match => {
|
|
return match[1].split('/').pop()!;
|
|
}).filter(Boolean);
|
|
|
|
await Promise.all(
|
|
imageNames.map(name => {
|
|
self.uploader.deleteFile(name).catch(error => {
|
|
new Notice("Image delete failed:" + error.message);
|
|
})
|
|
})
|
|
);
|
|
|
|
const newContent = allContent.replace(/!\[.*?\]\((.*?)\)/g, "");
|
|
editor.setValue(newContent);
|
|
|
|
new Notice(`Deleted ${imageNames.length} images!`);
|
|
})
|
|
});
|
|
})
|
|
);
|
|
|
|
this.addSettingTab(new CosPicbedSettingTab(this.app, this, () => { }));
|
|
this.registerInterval(window.setInterval(() => { }, 5 * 60 * 1000));
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, 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);
|
|
}
|
|
} |