mirror of
https://github.com/al0cam/AutoMover.git
synced 2026-07-22 05:41:25 +00:00
feat: counting moves passed | failed
This commit is contained in:
parent
450605fd66
commit
a31fdaf936
6 changed files with 318 additions and 94 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import type { AutoMoverSettings } from "Settings/Settings";
|
||||
import { Notice, TFile } from "obsidian";
|
||||
import loggerUtil from "Utils/LoggerUtil";
|
||||
import { TFile } from "obsidian";
|
||||
import type { App } from "obsidian";
|
||||
|
||||
class SettingsIO {
|
||||
|
|
@ -8,6 +9,11 @@ class SettingsIO {
|
|||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Returns the singleton instance of SettingsIO, creating it on first access.
|
||||
*
|
||||
* @returns SettingsIO
|
||||
*/
|
||||
public static getInstance(): SettingsIO {
|
||||
if (!SettingsIO.instance) {
|
||||
SettingsIO.instance = new SettingsIO();
|
||||
|
|
@ -15,6 +21,12 @@ class SettingsIO {
|
|||
return SettingsIO.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the Obsidian app reference used for vault and dialog access.
|
||||
*
|
||||
* @param app - The Obsidian App instance
|
||||
* @returns void
|
||||
*/
|
||||
public init(app: App): void {
|
||||
this.app = app;
|
||||
}
|
||||
|
|
@ -57,11 +69,10 @@ class SettingsIO {
|
|||
const fs = require("node:fs");
|
||||
fs.writeFileSync(filePath, settingsData);
|
||||
|
||||
new Notice("Settings exported successfully");
|
||||
loggerUtil.infoNotice("Settings exported successfully");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to export settings:", error);
|
||||
new Notice("Failed to export settings");
|
||||
loggerUtil.errorNotice("Failed to export settings", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -77,11 +88,10 @@ class SettingsIO {
|
|||
try {
|
||||
const filename = "AutoMover_settings.json";
|
||||
await this.app.vault.create(filename, settingsData);
|
||||
new Notice(`Settings exported to vault: ${filename}`);
|
||||
loggerUtil.infoNotice(`Settings exported to vault: ${filename}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to export to vault:", error);
|
||||
new Notice("Failed to export settings to vault");
|
||||
loggerUtil.errorNotice("Failed to export settings to vault", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -123,15 +133,14 @@ class SettingsIO {
|
|||
const importedSettings = JSON.parse(fileContent);
|
||||
|
||||
if (!this.validateSettings(importedSettings)) {
|
||||
new Notice("Invalid settings file format");
|
||||
loggerUtil.warnNotice("Invalid settings file format");
|
||||
return null;
|
||||
}
|
||||
|
||||
new Notice("Settings imported successfully");
|
||||
loggerUtil.infoNotice("Settings imported successfully");
|
||||
return importedSettings;
|
||||
} catch (error) {
|
||||
console.error("Failed to import settings:", error);
|
||||
new Notice("Failed to import settings");
|
||||
loggerUtil.errorNotice("Failed to import settings", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -148,7 +157,7 @@ class SettingsIO {
|
|||
const file = this.app.vault.getAbstractFileByPath(filename);
|
||||
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
new Notice(`Could not find ${filename} in vault`);
|
||||
loggerUtil.warnNotice(`Could not find ${filename} in vault`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -156,15 +165,14 @@ class SettingsIO {
|
|||
const importedSettings = JSON.parse(fileContent);
|
||||
|
||||
if (!this.validateSettings(importedSettings)) {
|
||||
new Notice("Invalid settings file format");
|
||||
loggerUtil.warnNotice("Invalid settings file format");
|
||||
return null;
|
||||
}
|
||||
|
||||
new Notice("Settings imported from vault successfully");
|
||||
loggerUtil.infoNotice("Settings imported from vault successfully");
|
||||
return importedSettings;
|
||||
} catch (error) {
|
||||
console.error("Failed to import from vault:", error);
|
||||
new Notice("Failed to import settings from vault");
|
||||
loggerUtil.errorNotice("Failed to import settings from vault", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface AutoMoverSettings {
|
|||
projectRules: ProjectRule[];
|
||||
automaticMoving: boolean;
|
||||
timer: number | null; // in miliseconds
|
||||
debugLogging: boolean;
|
||||
collapseSections: {
|
||||
tutorial: boolean;
|
||||
movingRules: boolean;
|
||||
|
|
@ -30,6 +31,7 @@ export const DEFAULT_SETTINGS: Partial<AutoMoverSettings> = {
|
|||
projectRules: [],
|
||||
automaticMoving: false,
|
||||
timer: null,
|
||||
debugLogging: false,
|
||||
collapseSections: {
|
||||
tutorial: false,
|
||||
movingRules: false,
|
||||
|
|
@ -39,6 +41,12 @@ export const DEFAULT_SETTINGS: Partial<AutoMoverSettings> = {
|
|||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads the plugin settings by merging persisted data on top of DEFAULT_SETTINGS.
|
||||
*
|
||||
* @param AutoMoverPlugin - The plugin instance used to read persisted data
|
||||
* @returns Partial<AutoMoverSettings>
|
||||
*/
|
||||
function loadSettings(AutoMoverPlugin: AutoMoverPlugin): Partial<AutoMoverSettings> {
|
||||
return Object.assign({}, DEFAULT_SETTINGS, AutoMoverPlugin.loadData());
|
||||
}
|
||||
|
|
|
|||
122
Utils/LoggerUtil.ts
Normal file
122
Utils/LoggerUtil.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import type AutoMoverPlugin from "main";
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
class LoggerUtil {
|
||||
private static instance: LoggerUtil;
|
||||
private static readonly PREFIX = "[AutoMover]";
|
||||
private static readonly NOTICE_DURATION_MS = 5000;
|
||||
private plugin: AutoMoverPlugin | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Returns the singleton instance of LoggerUtil, creating it on first access.
|
||||
*
|
||||
* @returns LoggerUtil
|
||||
*/
|
||||
public static getInstance(): LoggerUtil {
|
||||
if (!LoggerUtil.instance) {
|
||||
LoggerUtil.instance = new LoggerUtil();
|
||||
}
|
||||
return LoggerUtil.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires the logger to the plugin so it can read the current debug toggle
|
||||
* from settings on every call (settings may be reassigned on reload).
|
||||
*/
|
||||
public init(plugin: AutoMoverPlugin): void {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the current debug toggle from plugin settings.
|
||||
*
|
||||
* @returns true if debug logging is enabled, false otherwise
|
||||
*/
|
||||
private isDebugEnabled(): boolean {
|
||||
return this.plugin?.settings?.debugLogging === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a debug message to the console, prefixed with the plugin tag.
|
||||
* No-op when debug logging is disabled in settings.
|
||||
*
|
||||
* @param args - Values to log
|
||||
* @returns void
|
||||
*/
|
||||
public debug(...args: unknown[]): void {
|
||||
if (!this.isDebugEnabled()) return;
|
||||
console.debug(LoggerUtil.PREFIX, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an informational message to the console, prefixed with the plugin tag.
|
||||
*
|
||||
* @param args - Values to log
|
||||
* @returns void
|
||||
*/
|
||||
public info(...args: unknown[]): void {
|
||||
console.info(LoggerUtil.PREFIX, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning message to the console, prefixed with the plugin tag.
|
||||
*
|
||||
* @param args - Values to log
|
||||
* @returns void
|
||||
*/
|
||||
public warn(...args: unknown[]): void {
|
||||
console.warn(LoggerUtil.PREFIX, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error message to the console, prefixed with the plugin tag.
|
||||
*
|
||||
* @param args - Values to log
|
||||
* @returns void
|
||||
*/
|
||||
public error(...args: unknown[]): void {
|
||||
console.error(LoggerUtil.PREFIX, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Console.info + Obsidian Notice. `message` shows in both places; any
|
||||
* additional args (e.g. an Error or rule object) are appended to the
|
||||
* console line only.
|
||||
*/
|
||||
public infoNotice(message: string, ...extra: unknown[]): void {
|
||||
console.info(LoggerUtil.PREFIX, message, ...extra);
|
||||
new Notice(message, LoggerUtil.NOTICE_DURATION_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Console.warn + Obsidian Notice. `message` shows in both places; any
|
||||
* additional args are appended to the console line only.
|
||||
*
|
||||
* @param message - The user-facing warning message
|
||||
* @param extra - Optional values appended to the console line only
|
||||
* @returns void
|
||||
*/
|
||||
public warnNotice(message: string, ...extra: unknown[]): void {
|
||||
console.warn(LoggerUtil.PREFIX, message, ...extra);
|
||||
new Notice(message, LoggerUtil.NOTICE_DURATION_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Console.error + Obsidian Notice. `message` shows in both places; any
|
||||
* additional args (e.g. an Error or rule object) are appended to the
|
||||
* console line only.
|
||||
*
|
||||
* @param message - The user-facing error message
|
||||
* @param extra - Optional values appended to the console line only
|
||||
* @returns void
|
||||
*/
|
||||
public errorNotice(message: string, ...extra: unknown[]): void {
|
||||
console.error(LoggerUtil.PREFIX, message, ...extra);
|
||||
new Notice(message, LoggerUtil.NOTICE_DURATION_MS);
|
||||
}
|
||||
}
|
||||
|
||||
const loggerUtil = LoggerUtil.getInstance();
|
||||
export default loggerUtil;
|
||||
|
|
@ -1,6 +1,18 @@
|
|||
import { Notice, TFile, TFolder } from "obsidian";
|
||||
import loggerUtil from "Utils/LoggerUtil";
|
||||
import { TFile, TFolder } from "obsidian";
|
||||
import type { App } from "obsidian";
|
||||
|
||||
/**
|
||||
* Result of a single `MovingUtil.moveFile` call.
|
||||
*
|
||||
* - `moved` — file was renamed successfully
|
||||
* - `no_op` — file is already at the destination, nothing to do
|
||||
* - `duplicate` — a different file already exists at the destination path
|
||||
* - `invalid_path` — destination folder didn't exist and could not be created
|
||||
* - `error` — `vault.rename` rejected for any other reason (logged to console)
|
||||
*/
|
||||
export type MoveOutcome = "moved" | "no_op" | "duplicate" | "invalid_path" | "error";
|
||||
|
||||
class MovingUtil {
|
||||
private static instance: MovingUtil;
|
||||
private app: App;
|
||||
|
|
@ -40,26 +52,46 @@ class MovingUtil {
|
|||
}
|
||||
|
||||
/**
|
||||
* Moves the file to the destination path
|
||||
* Moves a file into the destination folder, preserving its name.
|
||||
*
|
||||
* Behavior:
|
||||
* - If the file is already at the destination, returns `"no_op"` without touching the vault.
|
||||
* - If another file already occupies the destination path, returns `"duplicate"`
|
||||
* (checked up-front via `getAbstractFileByPath` rather than by parsing rename errors).
|
||||
* - If the destination folder doesn't exist, intermediate folders are created; failure
|
||||
* to create them returns `"invalid_path"`.
|
||||
* - Any other rename rejection is caught, logged, and returned as `"error"`.
|
||||
*
|
||||
* @param file - File to be moved
|
||||
* @param newPath - Destination path
|
||||
* @returns void
|
||||
* @param newPath - Destination folder (the file's own name is appended automatically)
|
||||
* @returns a {@link MoveOutcome} describing what happened
|
||||
*/
|
||||
public moveFile(file: TFile, newPath: string): void {
|
||||
if (this.isFolder(newPath)) {
|
||||
this.app.vault.rename(file, `${newPath}/${file.name}`);
|
||||
} else {
|
||||
new Notice(
|
||||
`Invalid destination path\n${newPath} is not a folder!\nCreating requested folder.`,
|
||||
5000,
|
||||
);
|
||||
console.error(
|
||||
`Invalid destination path\n${newPath} is not a folder!\nCreating requested folder.`,
|
||||
);
|
||||
this.createMissingFolders(newPath).then(() => {
|
||||
this.app.vault.rename(file, `${newPath}/${file.name}`);
|
||||
});
|
||||
public async moveFile(file: TFile, newPath: string): Promise<MoveOutcome> {
|
||||
const targetPath = `${newPath}/${file.name}`;
|
||||
|
||||
if (file.path === targetPath) {
|
||||
return "no_op";
|
||||
}
|
||||
|
||||
if (this.app.vault.getAbstractFileByPath(targetPath)) {
|
||||
loggerUtil.debug(`Duplicate at "${targetPath}", skipping "${file.path}"`);
|
||||
return "duplicate";
|
||||
}
|
||||
|
||||
if (!this.isFolder(newPath)) {
|
||||
const created = await this.createMissingFolders(newPath);
|
||||
if (!created) {
|
||||
loggerUtil.error(`Could not create destination folder "${newPath}"`);
|
||||
return "invalid_path";
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.app.vault.rename(file, targetPath);
|
||||
return "moved";
|
||||
} catch (e) {
|
||||
loggerUtil.error(`Failed to move "${file.path}" -> "${targetPath}": ${e}`);
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,8 +106,7 @@ class MovingUtil {
|
|||
if (this.isFolder(newPath)) {
|
||||
this.app.vault.rename(folder, `${newPath}/${folder.name}`);
|
||||
} else {
|
||||
new Notice(`Invalid destination path\n${newPath} is not a folder!`, 5000);
|
||||
console.error(`Invalid destination path\n${newPath} is not a folder!`);
|
||||
loggerUtil.errorNotice(`Invalid destination path\n${newPath} is not a folder!`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,8 +122,7 @@ class MovingUtil {
|
|||
return folder;
|
||||
});
|
||||
} else {
|
||||
new Notice("Folder already exists", 5000);
|
||||
console.error("Folder already exists");
|
||||
loggerUtil.errorNotice("Folder already exists");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { MovingRule } from "Models/MovingRule";
|
||||
import { ProjectRule } from "Models/ProjectRule";
|
||||
import loggerUtil from "Utils/LoggerUtil";
|
||||
import type { TFile } from "obsidian";
|
||||
|
||||
class RuleMatcherUtil {
|
||||
|
|
@ -8,6 +9,11 @@ class RuleMatcherUtil {
|
|||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Returns the singleton instance of RuleMatcherUtil, creating it on first access.
|
||||
*
|
||||
* @returns RuleMatcherUtil
|
||||
*/
|
||||
public static getInstance(): RuleMatcherUtil {
|
||||
if (!RuleMatcherUtil.instance) {
|
||||
RuleMatcherUtil.instance = new RuleMatcherUtil();
|
||||
|
|
@ -38,15 +44,15 @@ class RuleMatcherUtil {
|
|||
public getMatchingRuleByName(file: TFile, rules: MovingRule[]): MovingRule | null {
|
||||
for (const rule of rules) {
|
||||
if (rule.regex == null || rule.regex === "") {
|
||||
console.error("Rule does not have a regex: ", rule);
|
||||
loggerUtil.error("Rule does not have a regex: ", rule);
|
||||
continue;
|
||||
}
|
||||
if (rule.folder == null || rule.folder === "") {
|
||||
console.error("Rule does not have a destination folder: ", rule);
|
||||
loggerUtil.error("Rule does not have a destination folder: ", rule);
|
||||
continue;
|
||||
}
|
||||
if (!this.isValidRegex(rule.regex)) {
|
||||
console.error("Rule has an invalid regex: ", rule);
|
||||
loggerUtil.error("Rule has an invalid regex: ", rule);
|
||||
continue;
|
||||
}
|
||||
const regex = this.getCompiledRegex(rule.regex);
|
||||
|
|
@ -67,15 +73,15 @@ class RuleMatcherUtil {
|
|||
public getMatchingRuleByTag(tags: string[], rules: MovingRule[]): MovingRule | null {
|
||||
for (const rule of rules) {
|
||||
if (rule.regex == null || rule.regex === "") {
|
||||
console.error("Tag Rule does not have a regex: ", rule);
|
||||
loggerUtil.error("Tag Rule does not have a regex: ", rule);
|
||||
continue;
|
||||
}
|
||||
if (rule.folder == null || rule.folder === "") {
|
||||
console.error("Tag Rule does not have a destination folder: ", rule);
|
||||
loggerUtil.error("Tag Rule does not have a destination folder: ", rule);
|
||||
continue;
|
||||
}
|
||||
if (!this.isValidRegex(rule.regex)) {
|
||||
console.error("Tag Rule has an invalid regex: ", rule);
|
||||
loggerUtil.error("Tag Rule has an invalid regex: ", rule);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +110,14 @@ class RuleMatcherUtil {
|
|||
return matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the regex groups from the first tag that matches the rule
|
||||
* If no tag matches, returns null
|
||||
*
|
||||
* @param tags - The list of tags to test
|
||||
* @param rule - The rule whose regex is applied to each tag
|
||||
* @returns RegExpMatchArray | null
|
||||
*/
|
||||
public getGroupMatchesForTags(tags: string[], rule: MovingRule): RegExpMatchArray | null {
|
||||
const regex = this.getCompiledRegex(rule.regex);
|
||||
// console.log("Compiled regex: ", regex);
|
||||
|
|
@ -130,7 +144,7 @@ class RuleMatcherUtil {
|
|||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
console.error(`Invalid regex pattern: ${e.message}`);
|
||||
loggerUtil.error(`Invalid regex pattern: ${e.message}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
140
main.ts
140
main.ts
|
|
@ -2,7 +2,8 @@ import settingsIO from "IO/SettingsIO";
|
|||
import * as Settings from "Settings/Settings";
|
||||
import { SettingsTab } from "Settings/SettingsTab";
|
||||
import exclusionMatcherUtil from "Utils/ExclusionMatcherUtil";
|
||||
import movingUtil from "Utils/MovingUtil";
|
||||
import loggerUtil from "Utils/LoggerUtil";
|
||||
import movingUtil, { type MoveOutcome } from "Utils/MovingUtil";
|
||||
import ruleMatcherUtil from "Utils/RuleMatcherUtil";
|
||||
import timerUtil from "Utils/TimerUtil";
|
||||
import * as obsidian from "obsidian";
|
||||
|
|
@ -11,6 +12,13 @@ import projectMatcherUtil from "Utils/ProjectMatcherUtil";
|
|||
export default class AutoMoverPlugin extends obsidian.Plugin {
|
||||
settings: Settings.AutoMoverSettings;
|
||||
|
||||
/**
|
||||
* Obsidian lifecycle hook called when the plugin is enabled.
|
||||
* Initializes utilities, loads settings, registers the settings tab,
|
||||
* file-open and automatic-moving events, the command, and the ribbon icon.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
async onload() {
|
||||
// console.log("loading plugin");
|
||||
movingUtil.init(this.app);
|
||||
|
|
@ -19,6 +27,7 @@ export default class AutoMoverPlugin extends obsidian.Plugin {
|
|||
* Loading settings and creating the settings tab
|
||||
*/
|
||||
this.settings = Object.assign({}, Settings.DEFAULT_SETTINGS, await this.loadData());
|
||||
loggerUtil.init(this);
|
||||
this.addSettingTab(new SettingsTab(this.app, this));
|
||||
|
||||
this.automaticMoving();
|
||||
|
|
@ -39,7 +48,7 @@ export default class AutoMoverPlugin extends obsidian.Plugin {
|
|||
this.registerEvent(
|
||||
// since i am defining my own event, ts-lint is crying about it but it works in the end
|
||||
this.app.workspace.on("AutoMover:automatic-moving-update", () => {
|
||||
console.log("Automatic moving update");
|
||||
loggerUtil.debug("Automatic moving update");
|
||||
this.automaticMoving();
|
||||
}),
|
||||
);
|
||||
|
|
@ -75,15 +84,34 @@ export default class AutoMoverPlugin extends obsidian.Plugin {
|
|||
*
|
||||
* @returns void
|
||||
*/
|
||||
goThroughAllFiles() {
|
||||
async goThroughAllFiles() {
|
||||
// console.log("Going through all files");
|
||||
const files = this.app.vault.getFiles();
|
||||
for (const file of files) {
|
||||
const candidates: obsidian.TFile[] = [];
|
||||
for (const file of this.app.vault.getFiles()) {
|
||||
if (file == null || file.path == null) continue;
|
||||
if (this.isFileExcluded(file)) continue;
|
||||
this.matchAndMoveFile(file);
|
||||
candidates.push(file);
|
||||
}
|
||||
new obsidian.Notice("All files moved!", 5000);
|
||||
|
||||
const results = await Promise.all(candidates.map((file) => this.matchAndMoveFile(file)));
|
||||
|
||||
let moved = 0;
|
||||
let duplicate = 0;
|
||||
let failed = 0;
|
||||
for (const result of results) {
|
||||
if (result === "moved") moved++;
|
||||
else if (result === "duplicate") duplicate++;
|
||||
else if (result === "invalid_path" || result === "error") failed++;
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
if (moved > 0) lines.push(`${moved} ${moved === 1 ? "file" : "files"} moved.`);
|
||||
if (duplicate > 0)
|
||||
lines.push(`${duplicate} ${duplicate === 1 ? "file" : "files"} not moved - duplicate name.`);
|
||||
if (failed > 0)
|
||||
lines.push(`${failed} ${failed === 1 ? "file" : "files"} failed to move.`);
|
||||
|
||||
loggerUtil.infoNotice(lines.length === 0 ? "No files needed to be moved." : lines.join("\n"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -100,53 +128,56 @@ export default class AutoMoverPlugin extends obsidian.Plugin {
|
|||
}
|
||||
|
||||
/**
|
||||
* Matches the file to the rule and moves it to the destination folder
|
||||
* Matches the file to the first applicable rule (project, then name, then tag)
|
||||
* and moves it.
|
||||
*
|
||||
* @param file - File to be matched and moved
|
||||
* @returns void
|
||||
* @returns the MoveOutcome from the matched rule, or null if no rule matched
|
||||
*/
|
||||
matchAndMoveFile(file: obsidian.TFile): void {
|
||||
async matchAndMoveFile(file: obsidian.TFile): Promise<MoveOutcome | null> {
|
||||
// console.log("Moving file: ", file.path);
|
||||
if (this.matchAndMoveByProject(file)) return;
|
||||
else if (this.matchAndMoveByName(file)) return;
|
||||
else this.matchAndMoveByTag(file);
|
||||
const projectResult = await this.matchAndMoveByProject(file);
|
||||
if (projectResult !== null) return projectResult;
|
||||
|
||||
const nameResult = await this.matchAndMoveByName(file);
|
||||
if (nameResult !== null) return nameResult;
|
||||
|
||||
return this.matchAndMoveByTag(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the file by name and moves it to the destination folder
|
||||
* Matches the file by name and moves it to the destination folder.
|
||||
*
|
||||
* @param file - File to be matched and moved
|
||||
* @returns true if the file was moved, false otherwise
|
||||
* @returns the MoveOutcome, or null if no rule matched
|
||||
*/
|
||||
matchAndMoveByName(file: obsidian.TFile): boolean {
|
||||
async matchAndMoveByName(file: obsidian.TFile): Promise<MoveOutcome | null> {
|
||||
const rule = ruleMatcherUtil.getMatchingRuleByName(file, this.settings.movingRules);
|
||||
if (rule == null || rule.folder == null) return false;
|
||||
if (rule == null || rule.folder == null) return null;
|
||||
|
||||
if (ruleMatcherUtil.isRegexGrouped(rule)) {
|
||||
const matches = ruleMatcherUtil.getGroupMatches(file, rule);
|
||||
const finalDestinationPath = ruleMatcherUtil.constructFinalDesinationPath(rule, matches!);
|
||||
movingUtil.moveFile(file, finalDestinationPath);
|
||||
} else {
|
||||
movingUtil.moveFile(file, rule.folder);
|
||||
return movingUtil.moveFile(file, finalDestinationPath);
|
||||
}
|
||||
return true;
|
||||
return movingUtil.moveFile(file, rule.folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the file by tags it contains and moves it to the destination folder
|
||||
* Matches the file by tags it contains and moves it to the destination folder.
|
||||
*
|
||||
* @param file - File to be matched and moved
|
||||
* @returns true if the file was moved, false otherwise
|
||||
* @returns the MoveOutcome, or null if no rule matched
|
||||
*/
|
||||
matchAndMoveByTag(file: obsidian.TFile): boolean {
|
||||
async matchAndMoveByTag(file: obsidian.TFile): Promise<MoveOutcome | null> {
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
if (cache == null) return false;
|
||||
if (cache == null) return null;
|
||||
const tags = obsidian.getAllTags(cache);
|
||||
if (tags == null || tags.length === 0) return false;
|
||||
if (tags == null || tags.length === 0) return null;
|
||||
|
||||
const tagRule = ruleMatcherUtil.getMatchingRuleByTag(tags, this.settings.tagRules);
|
||||
|
||||
if (tagRule == null || tagRule.folder == null) return false;
|
||||
if (tagRule == null || tagRule.folder == null) return null;
|
||||
|
||||
if (ruleMatcherUtil.isRegexGrouped(tagRule)) {
|
||||
const matches = ruleMatcherUtil.getGroupMatchesForTags(tags, tagRule);
|
||||
|
|
@ -154,38 +185,43 @@ export default class AutoMoverPlugin extends obsidian.Plugin {
|
|||
// console.log("Tag rule: ", tagRule);
|
||||
// console.log("Tag matches: ", matches);
|
||||
const finalDestinationPath = ruleMatcherUtil.constructFinalDesinationPath(tagRule, matches!);
|
||||
movingUtil.moveFile(file, finalDestinationPath);
|
||||
} else {
|
||||
movingUtil.moveFile(file, tagRule.folder);
|
||||
return movingUtil.moveFile(file, finalDestinationPath);
|
||||
}
|
||||
return true;
|
||||
return movingUtil.moveFile(file, tagRule.folder);
|
||||
}
|
||||
|
||||
matchAndMoveByProject(file: obsidian.TFile): boolean {
|
||||
/**
|
||||
* Matches the file by its `Project` (or `project`) frontmatter value and
|
||||
* moves it according to the matched project rule. If the project rule has
|
||||
* inner rules, the first matching inner rule's destination is appended;
|
||||
* otherwise (or on a "./" destination) the file is moved to the project root.
|
||||
*
|
||||
* @param file - File to be matched and moved
|
||||
* @returns the MoveOutcome, or null if no project rule matched
|
||||
*/
|
||||
async matchAndMoveByProject(file: obsidian.TFile): Promise<MoveOutcome | null> {
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
if (cache == null) return false;
|
||||
if (cache.frontmatter == null) return false;
|
||||
if (cache.frontmatter.Project == null && cache.frontmatter.project == null) return false;
|
||||
if (cache == null) return null;
|
||||
if (cache.frontmatter == null) return null;
|
||||
if (cache.frontmatter.Project == null && cache.frontmatter.project == null) return null;
|
||||
|
||||
const projectName: string = cache.frontmatter.Project || cache.frontmatter.project;
|
||||
|
||||
const projectRule = projectMatcherUtil.getMatchingProjectRule(projectName, this.settings.projectRules);
|
||||
if (projectRule == null || projectRule.folder == null) return false;
|
||||
if (projectRule == null || projectRule.folder == null) return null;
|
||||
|
||||
// If no rules defined, move to project root
|
||||
if (projectRule.rules == null || projectRule.rules.length === 0) {
|
||||
console.log("No rules defined, moving to project root");
|
||||
movingUtil.moveFile(file, projectRule.folder);
|
||||
return true;
|
||||
loggerUtil.debug("No rules defined, moving to project root");
|
||||
return movingUtil.moveFile(file, projectRule.folder);
|
||||
}
|
||||
|
||||
const rule = ruleMatcherUtil.getMatchingRuleByName(file, projectRule.rules);
|
||||
|
||||
// If no rule matches or folder is "./", move to project root
|
||||
if (rule == null || rule.folder === "./") {
|
||||
console.log("No matching rule or './' destination, moving to project root");
|
||||
movingUtil.moveFile(file, projectRule.folder);
|
||||
return true;
|
||||
loggerUtil.debug("No matching rule or './' destination, moving to project root");
|
||||
return movingUtil.moveFile(file, projectRule.folder);
|
||||
}
|
||||
|
||||
// console.log("Project rule's moving rule found: ", rule);
|
||||
|
|
@ -194,20 +230,26 @@ export default class AutoMoverPlugin extends obsidian.Plugin {
|
|||
const matches = ruleMatcherUtil.getGroupMatches(file, rule);
|
||||
const ruleDesinationPath = ruleMatcherUtil.constructFinalDesinationPath(rule, matches!);
|
||||
const finalDestinationPath = projectMatcherUtil.constructProjectDestinationPath(projectRule, ruleDesinationPath);
|
||||
|
||||
movingUtil.moveFile(file, finalDestinationPath);
|
||||
} else {
|
||||
const finalDestinationPath = projectMatcherUtil.constructProjectDestinationPath(projectRule, rule.folder);
|
||||
|
||||
movingUtil.moveFile(file, finalDestinationPath);
|
||||
return movingUtil.moveFile(file, finalDestinationPath);
|
||||
}
|
||||
return true;
|
||||
const finalDestinationPath = projectMatcherUtil.constructProjectDestinationPath(projectRule, rule.folder);
|
||||
return movingUtil.moveFile(file, finalDestinationPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the plugin settings from disk, merged on top of DEFAULT_SETTINGS.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
async asyncloadSettings() {
|
||||
this.settings = Object.assign({}, Settings.DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Obsidian lifecycle hook called when the plugin is disabled or reloaded.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
async onunload() {
|
||||
// console.log("unloading plugin");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue