diff --git a/README.md b/README.md index b7f6ff5..95cae0e 100755 --- a/README.md +++ b/README.md @@ -25,7 +25,10 @@ Released under [MIT](/LICENSE) by [@jordanhandy](https://github.com/jordanhandy) This plugin allows you to automatically upload images, video, audio and raw files pasted to Obsidian directly into your Cloudinary account (instead of stored locally). Note: There is functionality for media manipulation in this plugin using Cloudinary's custom parameters ## How it Works +### Single File Upload ![Action GIF](https://res.cloudinary.com/dakfccuv5/image/upload/v1636859613/Nov-13-2021_22-11-40_bpei0d.gif) +### Multi-file Upload +![Multi File](https://res.cloudinary.com/dakfccuv5/video/upload/v1718021709/mass-note-upload_qnx5ar.mp4) ## Configuration 1. Disable Obsidian Safe Mode 2. Install the Plugin diff --git a/docs/cloudinary-duplication.md b/docs/cloudinary-duplication.md new file mode 100644 index 0000000..2299d0c --- /dev/null +++ b/docs/cloudinary-duplication.md @@ -0,0 +1,24 @@ +--- +label: Cloudinary Duplication +layout: default +order: 600 +author: Jordan Handy +icon: gear +--- +## Duplication in Cloudinary +Without proper configuration, it is highly likely that Cloudinary will not upload items as you expect. I recommend reading [Cloudinary documentation on Upload Presets](https://support.cloudinary.com/hc/en-us/articles/360016481620-What-are-Upload-presets-and-how-to-use-them). + +This doc will explain some of the most important pieces of information. + +### Use filename or externally-defined public ID +In your Upload Preset Settings under the Storage and Access menu, this option allows you to preserve filenames in Cloudinary. Normally, when you upload an item to Cloudinary, it is renamed with a unique public ID. If you would like to preserve filename, use this setting to allow for you to keep original filenames. The Obsidian plugin is configured such that filenames will be used if this setting is enabled. +![Externally defined public ID](https://res.cloudinary.com/dakfccuv5/image/upload/v1718108147/457f3fb2-f4c6-4235-8708-981ed138485d.png) + +## Unique Filename +If **disabled**, the Unique Filename option will guard against duplication. With this enabled, Cloudinary will append random characters to end string of every upload. This means that even if you preserve file names, no two uploads of the same file will match and every upload will be unique. This could lead to mass duplication. + +Keep this **disabled** so that if a file with the same filename already exists duplication is less-likely to occur +![Unique Filename](https://res.cloudinary.com/dakfccuv5/image/upload/v1718108164/46971520-e89b-4ae3-ad31-83a7790419d3.png) + + +Continue to [Plugin Commands](plugin-commands.md) \ No newline at end of file diff --git a/docs/configuring-cloudinary.md b/docs/configuring-cloudinary.md index c308ee6..b564c0c 100644 --- a/docs/configuring-cloudinary.md +++ b/docs/configuring-cloudinary.md @@ -5,7 +5,7 @@ order: 800 author: Jordan Handy icon: gear --- -## Cloudinary Confiuration Steps +## Cloudinary Configuration Steps 1. Log in to Cloudinary and find your Cloud Name here ![Cloudinary Dashboard](assets/cloudinary-dash.png) 2. Enable Unsigned Uploads @@ -21,4 +21,10 @@ When the preset is created, it will have a "name" associated with it. Use this !!! Note If you have a folder name already configured on Cloudinary under the settings for your specific upload preset (can be configured on Cloudinary itself), this folder setting will be ignored. -!!! \ No newline at end of file +!!! + +## Cloudinary and Potential Duplicate Uploads +I recommend double-checking all of your Cloudinary Upload Preset settings before you begin to use the plugin. +[!ref Cloudinary Duplication](cloudinary-duplication.md) + +Continue to [Plugin Commands](plugin-commands.md) \ No newline at end of file diff --git a/docs/configuring-the-plugin.md b/docs/configuring-the-plugin.md index d7c3910..fdef922 100644 --- a/docs/configuring-the-plugin.md +++ b/docs/configuring-the-plugin.md @@ -11,15 +11,16 @@ To configure the Plugin, you'll need the following information: - Cloudinary Cloud Name - Cloudinary Upload Preset - Cloudinary Folder Name +- Configure the behaviour that triggers a Cloudinary upload + - Copy/paste from clipboard + - Drag and Drop - OPTIONAL: [Default Transformation Options](https://cloudinary.com/documentation/transformation_reference) - OPTIONAL: Configure which asset types you want to be uploaded to Cloudinary - Raw (non-media type files) - Audio - Video - Images -- OPTIONAL: Configure the behaviour that triggers a Cloudinary upload - - Copy/paste from clipboard - - Drag and Drop +- OPTIONAL: Configure specific folders that different media types should be placed into By default, only images are set to be uploaded to Cloudinary via a copy/paste from the clipboard, but this can be altered ![Cloudinary plugin configuration](https://res.cloudinary.com/dakfccuv5/image/upload/v1716510330/obsidian/wwhysme8vdrz6syd5skp.png) diff --git a/docs/plugin-commands/backup-vault-assets.md b/docs/plugin-commands/backup-vault-assets.md new file mode 100644 index 0000000..4608849 --- /dev/null +++ b/docs/plugin-commands/backup-vault-assets.md @@ -0,0 +1,14 @@ +--- +label: Backup Vault Assets to Cloudinary +layout: default +order: 700 +author: Jordan Handy +icon: cloud +--- +## Backup All Vault Assets to Cloudinary + +!!!warning +This is an experimental feature. Your mileage may vary as to the success of this command because of the issues already outlined in [uploading single note content](upload-single-note-cloudinary.md) and [upload all note contents](upload-all-notes-cloudinary.md). This command attempts to read all "non-markdown" files in your vault and upload a copy of them to Cloudinary. The "Backup Folder" option is where these files are stored. The "Preserve File Paths" option attempts to maintain the same foldering structure you have in your vault, prepended with your specified backup folder as the root. +!!! + +[See note on Cloudinary Duplication](../cloudinary-duplication.md) \ No newline at end of file diff --git a/docs/plugin-commands/plugin-commands.md b/docs/plugin-commands/plugin-commands.md new file mode 100644 index 0000000..c677eab --- /dev/null +++ b/docs/plugin-commands/plugin-commands.md @@ -0,0 +1,15 @@ +--- +label: Plugin Commands +layout: default +order: 1000 +author: Jordan Handy +icon: gear +--- +## Plugin Commands + +The Cloudinary Uploader plugin has palette commands that can be run. Explore them below: +- [Upload single note assets to Cloudinary](upload-single-note-cloudinary.md) +- [Upload all note assets to Cloudinary](upload-all-notes-cloudinary.md) +- [Local asset file backup](backup-vault-assets.md) + +Continue to [Upload single note assets to Cloudinary](upload-single-note-cloudinary.md) \ No newline at end of file diff --git a/docs/plugin-commands/plugin-commands.yml b/docs/plugin-commands/plugin-commands.yml new file mode 100644 index 0000000..b0df7f2 --- /dev/null +++ b/docs/plugin-commands/plugin-commands.yml @@ -0,0 +1,4 @@ +label: Plugin Commands +order: 700 +icon: gear +expanded: true \ No newline at end of file diff --git a/docs/plugin-commands/upload-all-notes-cloudinary.md b/docs/plugin-commands/upload-all-notes-cloudinary.md new file mode 100644 index 0000000..0d4a0c6 --- /dev/null +++ b/docs/plugin-commands/upload-all-notes-cloudinary.md @@ -0,0 +1,15 @@ +--- +label: Upload all note assets to Cloudinary +layout: default +order: 800 +author: Jordan Handy +icon: cloud +--- +## Upload All Note Assets to Cloudinary + +Within the command palette, run the "Upload all note files to Cloudinary" option. This will take all local assets located in the vault and attempt to upload them to Cloudinary. + +See below for information: +[!ref Upload single note files](upload-single-note-cloudinary.md) + +Continue to [Backup Vault Assets](backup-vault-assets.md) \ No newline at end of file diff --git a/docs/plugin-commands/upload-single-note-cloudinary.md b/docs/plugin-commands/upload-single-note-cloudinary.md new file mode 100644 index 0000000..ae4a722 --- /dev/null +++ b/docs/plugin-commands/upload-single-note-cloudinary.md @@ -0,0 +1,27 @@ +--- +label: Upload single note assets to Cloudinary +layout: default +order: 900 +author: Jordan Handy +icon: cloud +--- +## Upload Single Note Assets to Cloudinary + +Within the command palette, run the "Upload files in current note to Cloudinary" option. This will take all local assets located in the current note and attempt to upload them to Cloudinary. + +!!!warning +It is **strongly** recommended to take a backup of your notes before you perform this action. +As part of the action, your local media assets are **not deleted** when the upload to Cloudinary happens, so you can still reference them if you wanted to. However, because of the nature of the upload, and the variability of syntax in how some users may choose to reference media, this may alter the formatting of your notes. Use with caution. +!!! + +The success / failure of this plugin largely depends on the following factors: +1. Your Cloudinary plan -- Certain Cloudinary plans have quotas applied to their accounts. Consult Cloudinary documentation to understand what your limits are +2. The file types being uploaded -- Cloudinary only accepts certain file types to be uploaded to their servers. If you try to upload an unsupported file type, the upload for that specific file will fail. Consult [this documentation on media types](https://support.cloudinary.com/hc/en-us/articles/202520642-What-type-of-image-video-and-audio-formats-do-you-support) and [this documentation on raw types](https://support.cloudinary.com/hc/en-us/articles/202520572-Using-Cloudinary-for-files-other-than-images-and-videos) for more information. + +## Warning +When you first use the option for mass note upload, you will be presented with a warning message explaining the potential dangers of your action. By default, this warning message will be displayed **every time** you invoke the action. If you would like to disable this message, toggle the "Hide command palette mass upload warning" option in plugin settings. +## Demo +View a demo of the command below: +![Demo video - mass upload](https://res.cloudinary.com/dakfccuv5/video/upload/v1718021709/mass-note-upload_qnx5ar.mp4) + +Continue to [Uploading all note assets to Cloudinary](upload-all-notes-cloudinary.md) \ No newline at end of file diff --git a/manifest.json b/manifest.json index 725eec8..b321456 100755 --- a/manifest.json +++ b/manifest.json @@ -5,5 +5,5 @@ "author": "Jordan Handy", "isDesktopOnly": true, "minAppVersion": "0.11.0", - "version": "0.3.1" + "version": "1.0.0" } diff --git a/package.json b/package.json index cd78e54..4600b94 100755 --- a/package.json +++ b/package.json @@ -14,12 +14,13 @@ "eslint": "^7.30.0", "obsidian": "obsidianmd/obsidian-api", "obsidian-plugin-cli": "^0.4.5", - "typescript": "^4.1.5" + "typescript": "^4.1.5", + "retypeapp": "^1.11.0" }, "dependencies": { "axios": "^0.21.1", + "cloudinary": "^2.2.0", "compressorjs": "^1.0.7", - "object-path": "^0.11.5", - "retypeapp": "^1.11.0" + "object-path": "^0.11.5" } } diff --git a/src/commands/utils.ts b/src/commands/utils.ts new file mode 100644 index 0000000..fc698d9 --- /dev/null +++ b/src/commands/utils.ts @@ -0,0 +1,189 @@ +/*--------- Utilities for Commands and async uploads -----------*/ + +import { Notice, FileSystemAdapter, TFile } from 'obsidian'; +import path from 'path'; +import objectPath from 'object-path' +import { NoteWarningModal } from '../note-warning-modal'; +import { v2 as cloudinary } from 'cloudinary'; +import { audioFormats, videoFormats, imageFormats } from '../formats'; +import CloudinaryUploader from '../main'; + +// Generates a modal when command is invoked +// References 'plugin' as we need access to settings on Cloudinary Uploader +export function uploadNoteModal(file?: TFile, type: string, plugin: CloudinaryUploader): void { + new NoteWarningModal(plugin.app, type, (result): void => { + if (result == 'true') { + if (file) { + uploadCurrentNoteFiles(file, plugin); // If a file was passed and modal agreed + return; + } else { + // If no file passed, but assets were to be uploaded + if (type == 'asset') { + fetchMessages(plugin); + //uploadVault(plugin); // Upload vault function + return; + } else if (type == 'note') { //! If no file passed, but 'notes' to be uploaded, this means all notes are requested. + const files = plugin.app.vault.getMarkdownFiles() + for (let file of files) { + uploadCurrentNoteFiles(file, plugin); + } + + } + } + } else { + return; + } + + }).open(); +} + +export async function uploadVault(plugin: CloudinaryUploader): Promise { + let successMessages = []; + let failureMessages = []; + //* Get all files in vault that are not + //* MD files, so they may be uploaded + const files = plugin.app.vault.getFiles() + new Notice("Upload of vault files started. Depending on your vault size, this could take a long while. Watch for error or success notices",0); + for (let file of files) { + if (file.extension != 'md') { + let filePath; + const adapter = plugin.app.vault.adapter; + if (adapter instanceof FileSystemAdapter) { + filePath = adapter.getFullPath(file.path); + } + await cloudinary.uploader.unsigned_upload(filePath, plugin.settings.uploadPreset, { + folder: plugin.settings.preserveBackupFilePath ? path.join(plugin.settings.backupFolder, path.dirname(file.path)) : plugin.settings.backupFolder, + resource_type: 'auto' + }).then(res=>{ + // add to success messages array + successMessages.push('success') + },err=>{ + // add to failure messages array + failureMessages.push(err.message); + }); + } + } + // send messages + return [successMessages,failureMessages]; +} + +//* This function fetches messages from the mass upload job +//* as this could take a while if the vault is larger +export async function fetchMessages(plugin:CloudinaryUploader) : Promise{ + // After the upload action completes then + // retrieve data and display messages based on results. + uploadVault(plugin).then((data)=>{ + if(data[0].length > 0 && data[1].length > 0){ + new Notice("Cloudinary vault asset backup: There was some success in uploading vault files, as well as some errors. Open Developer Tools for error information in Console",0); + for(let msg of data[1]){ + console.warn(msg); + } + }else if(data[0].length >0){ + new Notice("Cloudinary vault asset backup: This was successfully completed. No errors to report",0); + }else if(data[1].length > 0){ + new Notice("Cloudinary vault asset backup: This operation failed. Please try again",0); + } + }); +} +export function uploadCurrentNoteFiles(file: TFile, plugin: CloudinaryUploader): void { + //! Read a cached version of the file, then: + /* + * Find a RegEx match for [[]] file refs + * Get file name and find full path of file + * Pass full path to Cloudinary for upload + * Based on answer returned, determine subfolder, then: + * Replace current strings with Cloudinary URLs + */ + let data = plugin.app.vault.cachedRead(file).then(() => { + const found = data.match(/\!\[\[(?!https?:\/\/).*?\]\]/g); + if (found && found.length > 0) for (let find of found) { + let fileString = find.substring(3, find.length - 2); + let filePath; + const adapter = plugin.app.vault.adapter; + if (adapter instanceof FileSystemAdapter) { + filePath = adapter.getFullPath(fileString) + cloudinary.uploader.unsigned_upload(filePath, plugin.settings.uploadPreset, { + folder: setSubfolder(undefined, filePath, plugin), + resource_type: 'auto' + }).then(res => { + console.log(res); + let url = objectPath.get(res, 'secure_url'); + let resType = objectPath.get(res, 'resource_type'); + url = generateTransformParams(url, plugin); + let replaceMarkdownText = generateResourceUrl(resType, url); + data = data.replace(find, replaceMarkdownText); + plugin.app.vault.process(file, () => { + return data; + }) + new Notice("Upload of note file was completed"); // Success + }, err => { + // Failure + new Notice("There was something wrong with your upload. Please try again. " + file.name + '. ' + err.message, 0); + }) + } + } + }); +} +// Called to generate the output of the transformation parameters +// that are set on uploads +export function generateTransformParams(url: string, plugin: CloudinaryUploader): string { + if (plugin.settings.transformParams) { + const splitURL = url.split("/upload/", 2); + url = splitURL[0] += "/upload/" + plugin.settings.transformParams + "/" + splitURL[1]; + } + if (plugin.settings.f_auto) { + const splitURL = url.split("/upload/", 2); + url = splitURL[0] += "/upload/f_auto/" + splitURL[1]; + // leave standard of no transformations added + } + return url; +} +// Once upload completes, generate the resulting getMarkdown +// that will be written to file +export function generateResourceUrl(type: string, url: string): string { + if (type == 'audio' || isType(url, audioFormats)) { + return `\n`; + } else if (type == 'video' || isType(url, videoFormats)) { + return `\n`; + } else { + return `![](${url})`; + } +} +// Required as Cloudinary doesn't have an 'audio' resource type. +// As we only know the file type after it's been uploaded (we don't know MIME type), +// we check if audio was uploaded based on the most-commonly used audio formats +export function isType(url: string, formats: string[]): boolean { + let foundTypeMatch = false; + for (let format of formats) { + if (url.endsWith(format)) { + foundTypeMatch = true; + } + } + return foundTypeMatch; +} +// Determine the subfolder to place the uploaded +// file in, based on file type uploaded +export function setSubfolder(file: File, resourceUrl: string, plugin: CloudinaryUploader): string { + if (file) { + if (file.type && file.type.startsWith("image")) { + return `${plugin.settings.folder}/${plugin.settings.imageSubfolder}`; + } else if (file.type.startsWith("audio")) { + return `${plugin.settings.folder}/${plugin.settings.audioSubfolder}`; + } else if (file.type.startsWith("video")) { + return `${plugin.settings.folder}/${plugin.settings.videoSubfolder}`; + } else { + return `${plugin.settings.folder}/${plugin.settings.rawSubfolder}`; + } + } else if (resourceUrl) { + if (isType(resourceUrl, imageFormats)) { + return `${plugin.settings.folder}/${plugin.settings.imageSubfolder}`; + } else if (isType(resourceUrl, audioFormats)) { + return `${plugin.settings.folder}/${plugin.settings.audioSubfolder}`; + } else if (isType(resourceUrl, videoFormats)) { + return `${plugin.settings.folder}/${plugin.settings.videoSubfolder}`; + } else { + return `${plugin.settings.folder}/${plugin.settings.rawSubfolder}`; + } + } + +} diff --git a/src/formats.ts b/src/formats.ts new file mode 100644 index 0000000..d40f56d --- /dev/null +++ b/src/formats.ts @@ -0,0 +1,48 @@ +export const audioFormats: string[] = ["mp3", "wav", "m4a", "aac", "ogg", + "flac", "wma", "aiff", "ape", "opus", "amr", "pcm", "au", "ra", "mka", "ac3", "mid", "midi", + "mp2", "wv", "dts"]; + +export const imageFormats: string[] = [ + "jpg", + "jpeg", + "png", + "gif", + "tiff", + "bmp", + "svg", + "raw", + "pdf", + "psd", + "eps", + "jp2", + "webp", + "heic", + "ico", + "tga", + "pict", + "pcx", + "wmf", + "exif" +]; +export const videoFormats : string[] = [ + "mp4", + "mov", + "avi", + "mkv", + "wmv", + "flv", + "webm", + "m4v", + "mpeg", + "mpg", + "3gp", + "ogg", + "qt", + "asf", + "rm", + "rmvb", + "m2ts", + "ts", + "vob", + "divx" + ]; \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 58cf8e8..aa51848 100755 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,9 @@ import { // For API requests import axios from "axios" import objectPath from 'object-path' +import { v2 as cloudinary } from 'cloudinary'; +import { uploadVault, uploadNoteModal, uploadCurrentNoteFiles, setSubfolder, generateResourceUrl,generateTransformParams,fetchMessages } from "./commands/utils"; + // Settings tab import import CloudinaryUploaderSettingTab from './settings-tab' @@ -15,12 +18,55 @@ import { DEFAULT_SETTINGS, CloudinarySettings } from "./settings-tab"; export default class CloudinaryUploader extends Plugin { settings: CloudinarySettings; - private clearHandlers() { + private setCommands(): void { + this.addCommand({ + id: "upload-single-note-files-to-cloudinary", + name: "Upload files in current note to Cloudinary", + callback: () => { + let file = this.app.workspace.getActiveFile(); + if (this.settings.ignoreWarnings) { + uploadCurrentNoteFiles(file,this); + } else { + uploadNoteModal(file, 'note',this); + + } + } + }); + this.addCommand({ + id: "upload-all-note-files-cloudinary", + name: "Upload all note files to Cloudinary", + callback: () => { + const files = this.app.vault.getMarkdownFiles() + if (this.settings.ignoreWarnings) { + for (let file of files) { + uploadCurrentNoteFiles(file,this); + } + + } else { + uploadNoteModal(undefined, 'note',this); + } + + } + }); + this.addCommand({ + id: "upload-all-media-assets-cloudinary", + name: "Upload all vault media assets to Cloudinary", + callback: () => { + if (this.settings.ignoreWarnings) { + //async fetch messages after upload of vault assets + fetchMessages(this); + } else { + uploadNoteModal(undefined, 'asset',this); + } + } + }); + } + private clearHandlers(): void { this.app.workspace.off('editor-paste', this.pasteHandler); this.app.workspace.off('editor-drop', this.dropHandler); } - private setupHandlers() { + private setupHandlers(): void { if (this.settings.clipboardUpload) { this.registerEvent(this.app.workspace.on('editor-paste', this.pasteHandler)); } else { @@ -32,11 +78,11 @@ export default class CloudinaryUploader extends Plugin { this.app.workspace.off('editor-drop', this.dropHandler); } } - private pasteHandler = async (event: ClipboardEvent, editor: Editor) => { + private pasteHandler = async (event: ClipboardEvent, editor: Editor): Promise => { const { files } = event.clipboardData; await this.uploadFiles(files, event, editor); // to fix } - private dropHandler = async (event: DragEventInit, editor: Editor) => { + private dropHandler = async (event: DragEventInit, editor: Editor): Promise => { const { files } = event.dataTransfer; await this.uploadFiles(files, event, editor); // to fix } @@ -67,7 +113,7 @@ export default class CloudinaryUploader extends Plugin { const formData = new FormData(); formData.append('file', file); formData.append('upload_preset', this.settings.uploadPreset); - formData.append('folder', this.setSubfolder(file)); + formData.append('folder', this.settings.folder != '' ? setSubfolder(file, undefined,this) : ''); // Make API call axios({ @@ -77,30 +123,11 @@ export default class CloudinaryUploader extends Plugin { }).then(res => { // Get response public URL of uploaded image console.log(res); - let url = objectPath.get(res.data, 'secure_url') - let replaceMarkdownText = ""; - + let url = objectPath.get(res.data, 'secure_url'); + let resType = objectPath.get(res.data, 'resource_type'); // Split URL to allow for appending transformations - if (this.settings.transformParams) { - const splitURL = url.split("/upload/", 2); - url = splitURL[0] += "/upload/" + this.settings.transformParams + "/" + splitURL[1]; - replaceMarkdownText = `![](${url})`; - } - if (this.settings.f_auto) { - const splitURL = url.split("/upload/", 2); - url = splitURL[0] += "/upload/f_auto/" + splitURL[1]; - replaceMarkdownText = `![](${url})`; - - // leave standard of no transformations added - } else { - replaceMarkdownText = `![](${url})`; - } - // Change URL format based on content type - if (files[0].type.startsWith("audio")) { - replaceMarkdownText = `\n` - } else if (files[0].type.startsWith("video")) { - replaceMarkdownText = `\n` - } + url = generateTransformParams(url,this); + let replaceMarkdownText = generateResourceUrl(file.type, url); // Show MD syntax using uploaded image URL, in Obsidian Editor this.replaceText(editor, pastePlaceText, replaceMarkdownText) }, err => { @@ -117,18 +144,7 @@ export default class CloudinaryUploader extends Plugin { } } - // Set subfolder for upload - private setSubfolder(file: File) { - if (file.type.startsWith("image")) { - return `${this.settings.folder}/${this.settings.imageSubfolder}`; - } else if (file.type.startsWith("audio")) { - return `${this.settings.folder}/${this.settings.audioSubfolder}`; - } else if (file.type.startsWith("video")) { - return `${this.settings.folder}/${this.settings.videoSubfolder}`; - } else { - return `${this.settings.folder}/${this.settings.rawSubfolder}`; - } - } + // Function to replace text private replaceText(editor: Editor, target: string, replacement: string): void { target = target.trim(); @@ -146,15 +162,21 @@ export default class CloudinaryUploader extends Plugin { } } } - + // Plugin load steps async onload(): Promise { console.log("loading Cloudinary Uploader"); await this.loadSettings(); this.clearHandlers(); this.setupHandlers(); - this.addSettingTab(new CloudinaryUploaderSettingTab(this.app, this)); - } + this.addSettingTab(new CloudinaryUploaderSettingTab(this.app,this)); + + // Set cloudinary cloud name config for node module + cloudinary.config({ + cloud_name: this.settings.cloudName + }); + this.setCommands(); + } // Plugin shutdown steps onunload(): void { @@ -173,4 +195,4 @@ export default class CloudinaryUploader extends Plugin { this.clearHandlers(); this.setupHandlers(); } -} +} \ No newline at end of file diff --git a/src/note-warning-modal.ts b/src/note-warning-modal.ts new file mode 100644 index 0000000..29bc6be --- /dev/null +++ b/src/note-warning-modal.ts @@ -0,0 +1,59 @@ +import { App, Modal,Setting } from "obsidian"; + +export class NoteWarningModal extends Modal { + result: string; + type: string; + onSubmit: (result: string) => void; + constructor(app: App, type:string,onSubmit: (result:string)=> void) { + super(app); + this.onSubmit = onSubmit; + this.type = type; + } + + onOpen() { + let { contentEl } = this; + if(this.type == 'note'){ + contentEl.createEl("h1", { text: "Note Media Upload - Warning" }); + }else{ + contentEl.createEl("h1", { text: "Mass Asset Backup - Warning" }); + } + let textFragment = document.createDocumentFragment(); + textFragment.append("This is a potentially dangerous action."+ + " It is HIGHLY recommended that you backup this note elsewhere before performing this operation."+ + " The media files in this note will attempt to be uploaded to Cloudinary"); + contentEl.createEl("p", { text: textFragment }); + contentEl.createEl("h1", { text: "Other Information" }); + textFragment = document.createDocumentFragment(); + textFragment.append("As a precaution, your local files in your vault will NOT be deleted, and will still remain in your vault "+ + " if you need to reference them. The success of this action largely depends on the following:"); + contentEl.createEl("p", { text: textFragment }); + textFragment = document.createDocumentFragment(); + textFragment.append('Your Cloudinary account subscription plan -- different plans have different upload limits'); + contentEl.createEl("li", { text: textFragment }); + textFragment = document.createDocumentFragment(); + textFragment.append('The content you upload -- Certain files (example, .exe, .ps1) are not allowed to be uploaded to Cloudinary. If these are in your vault, the upload of these specific files will fail'); + contentEl.createEl("li", { text: textFragment }); + if(this.type == 'note'){ + textFragment.append('Should this process fail, or timeout, you can attempt to run the same command again as assets already uploaded to Cloudinary should not be reuploaded because of how the file search is completed for initial upload.') + }else{ + textFragment.append('Should this process fail, trying again will start fresh from the beginning. This is because there is currently no flag to denote if an upload has been completed from a previous attempt.' + + ' Additionally, depending on your Cloudinary settings (within the upload preset in the Cloudinary account, pre-existing files may be overwritten OR duplicated depending on these settings.'); + } + contentEl.createEl("p",{ text : textFragment }); + + new Setting(contentEl) + .addButton((btn) => + btn.setButtonText("Continue action") + .setCta() + .onClick(()=>{ + this.close(); + this.result = 'true'; + this.onSubmit(this.result) + })) + } + + onClose() { + let { contentEl } = this; + contentEl.empty(); + } +} \ No newline at end of file diff --git a/src/settings-tab.ts b/src/settings-tab.ts index ddeba62..af39eaa 100755 --- a/src/settings-tab.ts +++ b/src/settings-tab.ts @@ -26,6 +26,9 @@ export interface CloudinarySettings { audioSubfolder: string; videoSubfolder: string; rawSubfolder: string; + preserveBackupFilePath: boolean; + backupFolder: string; + ignoreWarnings: boolean; } export const DEFAULT_SETTINGS: CloudinarySettings = { cloudName: "", @@ -42,7 +45,10 @@ export const DEFAULT_SETTINGS: CloudinarySettings = { imageSubfolder: "", audioSubfolder: "", videoSubfolder: "", - rawSubfolder: "" + rawSubfolder: "", + preserveBackupFilePath: false, + backupFolder: "", + ignoreWarnings: false }; export default class CloudinaryUploaderSettingTab extends PluginSettingTab { @@ -330,5 +336,68 @@ export default class CloudinaryUploaderSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); }) }); + containerEl.createEl("h5", { text: "File names, file conflicts, overwrite behaviour" }); + link = document.createElement("a"); + link.text="plugin documentation "; + link.href="https://jordanhandy.github.io/obsidian-cloudinary-uploader/cloudinary-duplication/"; + textFragment = document.createDocumentFragment(); + textFragment.append("Assuming all defaults in your Cloudinary Upload Preset settings, all file backups will receive a unique public ID (file name) within the Cloudinary console."+ + " This may make it hard to identify. Additionally, file uploads will always be overwritten. You can use a combination of settings for unique file naming as found in "); + textFragment.append(link); + containerEl.createEl("p", { text: textFragment }); + + + containerEl.createEl("h4", { text: "Warnings" }); + new Setting(containerEl) + .setName("Hide command palette mass upload warning") + .setDesc("Hides the warning modal and assumes that all mass actions are approved") + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.ignoreWarnings) + .onChange(async (value) => { + try { + this.plugin.settings.ignoreWarnings = value; + await this.plugin.saveSettings(); + } + catch (e) { + console.log(e) + } + }) + }); + containerEl.createEl("h3", { text: "EXPERIMENTAL FEATURES" }); + containerEl.createEl("h4", { text: "Local File Backup" }); + textFragment = document.createDocumentFragment(); + textFragment.append("If you run the command to create a backup of vault local assets, these settings apply"); + containerEl.createEl("p", { text: textFragment }); + + new Setting(containerEl) + .setName("Backup folder") + .setDesc("Root folder where backups are stored. If not specified and you run a backup, root is specified as the root of your Cloudinary media library") + .addText((text) => { + text + .setPlaceholder("backups") + .setValue(this.plugin.settings.backupFolder) + .onChange(async (value) => { + this.plugin.settings.backupFolder = value; + await this.plugin.saveSettings(); + }) + }); + + new Setting(containerEl) + .setName("Preserve File Paths") + .setDesc("Preserve vault file path relative to root backup folder. If disabled, assets will be placed in 'root', whether the above backup folder or root of Cloudinary Media library") + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.preserveBackupFilePath) + .onChange(async (value) => { + try { + this.plugin.settings.preserveBackupFilePath = value; + await this.plugin.saveSettings(); + } + catch (e) { + console.log(e) + } + }) + }); } } \ No newline at end of file