关于使用QuickAdd脚本实现按创建时间归档Obsidian附件的方案

:warning:实验性脚本,对库的变动较大,谨慎使用,本文旨在为有需求的人提供方法

Obsidian的附件管理一直是个难题。我目前采用指定的附件文件夹来管理附件,并尽可能简短地使用WIKI链接。然而,随着笔记的增长,附件文件夹里已经有七千多张图片,每次打开都令人头疼。

我考虑采用PKMer群友之前讨论的,按照YYYY/YYYY-MM-DD的方式归档附件。但在Obsidian的插件市场中,似乎没有直接这个方法的附件管理插件。因此,我尝试使用QuickAdd脚本,以附件的创建时间为基础,进行文件夹分类归档,调用的是Obsidian自带的API进行文件移动,这样,附件链接会自动更新。实现效果如下:

2024-06-13_055036 关于使用QuickAdd脚本实现按创建时间归档Obsidian附件的方案_IMG-2

脚本的配置选项:

2024-06-13_055036 关于使用QuickAdd脚本实现按创建时间归档Obsidian附件的方案_IMG-3

  • 自定义文件夹:为空则为Obsidian 默认设置,也可以自定义路径
    • PS: 针对该文件夹路径下(不包含子文件夹)的符合**文件类型(file type)**的文件进行归档。
  • 归档日期格式YYYY/YYYY-MM-DD,可以预览
    • 2024-06-13_055036 关于使用QuickAdd脚本实现按创建时间归档Obsidian附件的方案_IMG-2
  • 文件类型:默认为:png|jpe?g|webp|mp[3,4]|pdf
    • 文件后缀类型,不同类型用|分隔,不区分大小写。
  • 自启动设置:可以在 QuickAdd 的 Macro Manager 设置自启动。

QuickAdd Macro 脚本

脚本配置流程:

按创建时间归档附件.js
/*
 * @Author: 熊猫别熬夜 
 * @Date: 2024-06-13 07:04:22 
 * @Last Modified by: 熊猫别熬夜
 * @Last Modified time: 2024-06-22 23:51:47
*/

module.exports = {
  entry: async (QuickAdd, settings, params) => {
    // 获取默认配置
    const fs = require('fs');
    const path = require('path');
    // 获取库的基本路径
    const basePath = (app.vault.adapter).getBasePath();

    // 日期格式
    const dateFormat = settings["归档日期格式(date format)"].replace("{{DATE:", "").replace("}}", "");

    // 获取设置的指定附件文件夹路径:
    // const assetsPath = "900【素材】Assets/910_ObsidianAssets";
    let assetsPath = app.vault.config.attachmentFolderPath;
    if (settings["自定义文件夹路径(custom folder path)"]) {
      assetsPath = settings["自定义文件夹路径(custom folder path)"];
    }

    const assetsPathFull = basePath + "/" + assetsPath;
    console.log(assetsPathFull);

    // 获取该路径下所有附件:
    const assetsList = fs.readdirSync(assetsPathFull).filter(file => {
      const ext = path.extname(file).toLowerCase();
      // 使用正则表达式来匹配文件扩展名:
      // 如果 settings["文件类型(file type)"] 为空值,则匹配所有文件类型
      const regexMatch = new RegExp("\\.(" + (settings["文件类型(file type)"] || ".+") + ")$", "i");
      return regexMatch.test(ext);
    }).map(file => assetsPath + "/" + file);

    console.log(assetsList);

    // 批量获取创建日期并用ob的API移动附件
    for (const filePath of assetsList) {
      const ctime = app.vault.getAbstractFileByPath(filePath).stat["ctime"];
      const formattedDatePath = assetsPath + "/" + moment(ctime).format(dateFormat);
      console.log(formattedDatePath);
      if (!await app.vault.getFolderByPath(formattedDatePath)) {
        await app.vault.createFolder(formattedDatePath);
      }

      const destinationPath = path.join(formattedDatePath, path.basename(filePath));
      await app.fileManager.renameFile(app.vault.getAbstractFileByPath(filePath), destinationPath);
    }
    new Notice(`🔊${assetsList.length}个附件归档已完成`);
  },
  settings: {
    name: "按创建时间归档Obsidian附件",
    author: "熊猫别熬夜",
    options: {
      "自定义文件夹路径(custom folder path)": {
        type: "text",
        defaultValue: "",
        description: "如果为空则为Obsidian默认附件存放路径"
      },
      "归档日期格式(date format)": {
        type: "format",
        defaultValue: "{{DATE:YYYY/YYYY-MM/YYYY-MM-DD}}",
        description: "如果想以文件类型分类,可以配置{{DATE:[图形文件]YYYY/YYYY-MM-DD}}、{{DATE:[视频文件]YYYY/YYYY-MM-DD}}",
      },
      "文件类型(file type)": {
        type: "text",
        defaultValue: "png|jpe?g|webp|gif|mp[34]|pdf",
        description: "文件后缀类型,不同类型用|分隔,不区分大小写,如果为空值则默认全部附件。"
      }
    }
  }
};

拓展方法:Python 附件归档

不过需要注意的是,QuickAdd的方法一次性只能处理少量附件,要处理大量附件的话可能会需要用很长时间,除了使用 QuickAdd 之外,还可以通过 Python 脚本实现附件归档。但该方式是在库外移动文件位置,这样会导致链接失效。如果您的库全部使用的是简短的 WIKI 链接格式,这种方法可以初步更快地进行归档,在修改后重启 Obsidian 就能修复,之后可以采用第一种方法。

:orange_circle:Python脚本是库外移动文件,会导致链接失效,不推荐使用

保存为 00_ObsidianAssets.py 存放在 Obsidian 默认附件文件夹。

00_ObsidianAssets.py
import os
import shutil
from datetime import datetime

# 获取当前文件夹路径
current_dir = os.getcwd()

# 遍历当前文件夹下的所有文件
for file_name in os.listdir(current_dir):
    if file_name.lower().endswith(('.jpg', '.jpeg', '.png', '.pdf')):
        file_path = os.path.join(current_dir, file_name)
        # 获取文件的创建日期
        create_time = datetime.fromtimestamp(os.path.getctime(file_path))
        folder_name = create_time.strftime('%Y/%Y-%m/%Y-%m-%d')
        folder_path = os.path.join(current_dir, folder_name)

        # 创建文件夹
        os.makedirs(folder_path, exist_ok=True)

        # 移动文件到对应的文件夹
        shutil.move(file_path, os.path.join(folder_path, file_name))

可以设置 .bat 进行运行:

@echo off
python "%cd%\00_ObsidianAssets.py"
exit
2 个赞

可以在库外调用obsidian的api移动,比如,我想到的方法是通过注册registerObsidianProtocolHandler回调处理文件的移动。
然后库外可以这样调用obsidian://move-file?vault=vault&file=file&to=path

1 个赞

可以用 Paste image rename 插件,可以带路径重命名图片附件,支持日期。随时分类存放。

抱歉,我从插件市场下的,如果设置附件是{{DATE:YYYY-MM-DD}}/{{fileName}}的话,它显示我无法进行重命名,因为里面包含/,请问该如何进行设置

这个插件只是重命名,不能新建文件夹,也就是说你需要先把文件夹建好。按照楼主按年归档,需要手动每年建个文件夹。
我的配置是按照时间戳命名图片:Images/{{DATE:YYYY/x}}

哦哦,原来如此,非常感谢解答,可能这个插件与我想要实现的效果稍微有点不一样,最好是年月日嵌套的格式

不过其实也算可以实现,直接用脚本在每次启动ob时创建对应文件夹,剩余的交给Paste image rename 插件也是可以的,我发布的这个QuickAdd脚本可以用来整理之前没整理的图片,让他们自动归档,可能像Canvas,Excalidraw里面的图片也无法用Paste image rename 进行归档,这个QuickAdd脚本也可以让它归档。

我现在用的是Attachment Name Formatting插件对附件进行重命名,主要以笔记名进行重命名,有时候图片的名称可以让我定位到笔记,时间戳格式的命名反而让我有点迷糊,虽然可以通过反向链接去识别,但还是笔记名对附件重命对我来说要好点。

似乎附件文件夹也需要一个类似日记路径的解析方式 :joy:


看了一下,两年前就有这样的需求了()

1 个赞

yep,如果官方能直接加入这个解析功能就好了

说起来我之前改过 GitHub - trganda/obsidian-attachment-management: Attachment Management of Obsidian
让它支持/,应该可以用类似的思路来

// calmwaves
renameCreateFile(attach, attachPath, attachName, source) {
  const dst = (0, import_obsidian10.normalizePath)(path.join(attachPath, attachName));
  //console.log("测试dst"+dst) // calmwaves
  debugLog("renameFile - ", attach.path, " to ", dst);
  const original = attach.basename;
  const name = attach.name;
  // 创建目标路径中的目录
  this.createDirectoriesForPath(dst).then(() => {
    // 目录创建完成后,重命名文件
    this.app.fileManager.renameFile(attach, dst).then(() => {
      new import_obsidian10.Notice(`Renamed ${name} to${attachName}.`);
    }).finally(() => {
      const { setting } = getOverrideSetting(this.settings, source);
      MD5(this.app.vault.adapter, attach).then((md5) => {
        saveOriginalName(this.settings, setting, attach.extension, {
          n: original,
          md5
        });
        this.plugin.saveData(this.settings);
      });
    });
  });
}

// 新增的函数,用于创建目标路径中的目录
createDirectoriesForPath(filePath) {
  return new Promise((resolve, reject) => {
    const directory = path.dirname(filePath);
    this.app.vault.adapter.exists(directory).then(exists => {
      if (exists) {
        resolve();
      } else {
        this.app.vault.createFolder(directory).then(() => {
          resolve();
        }).catch(error => {
          reject(error);
        });
      }
    });
  });
}
2 个赞

哦哦 厉害了 有时间研究下 终究上升到了DIY插件 :joy:

现在附件有几个来源
1.剪切板复制粘贴进去的
2.鼠标拖拽进去的
3.网络采集的图片然后用local image下载的

对1和2都可以用插件解决,有一个插件叫custom attachment location
对3,其实custom attachment location也能做,它有一个功能叫collect attachments,可以把附件按照配置归档,但是它太霸道了,没法设置例外,已经归档的也会重新归档,而且时间时归档时间不是创建时间

所以楼主的脚本更好用,因为首先它只会归档目录第一层的文件,不会递归,影响小(也要感谢nodejs的readdir默认不是递归的)。第二就是用的是创建时间。

总之结合一下完美实现了我的需求,谢谢楼主!

1 个赞

补充一个配合用了很久的附件重命名的脚本,虽然插件市场重命名的插件很多,不过我使用这2个脚本就已经满足我自己的需求了,有需要可测试一下。

脚本功能:重命名当前文件的附件,附件名称以文件的创建时间的时间戳命名File-YYYYMMDDhhmmssSSS,已经命名的就不会重命名,用来统一一篇笔记的附件文件名,特别是那种QQ复制过来的。推荐设置的快捷键是F1,在Excalidraw画板操作中也起作用。

注意事项:因为采用的是Ob的API,所以附件重命名后的位置与ob设置的附件管理的位置一致,如果你的图片在其它文件夹时,被重命名后,会自动移动到ob的附件文件夹。不过这个特性挺好用的。

QuickAddMacro:24.08.17_时间戳统一附件.js
/*
 * @Author: 熊猫别熬夜 
 * @Date: 2024-08-17 00:49:04 
 * @Last Modified by: 熊猫别熬夜
 * @Last Modified time: 2024-11-04 19:55:36
 */

// 导入所需模块
const path = require('path');
const files = app.vault.getFiles();
// 获取当前活动文件和缓存的元数据
const file = app.workspace.getActiveFile();
const cachedMetadata = app.metadataCache.getFileCache(file);
let listPaths = app.vault.getAllFolders().map(f => f.path);
listPaths.unshift("/");

// 导出异步函数
module.exports = async () => {
  let links = [];
  let embeds = [];
  // 提取链接和嵌入的文件
  if (cachedMetadata.links) {
    links = cachedMetadata.links.map(l => l.link);
  }
  if (cachedMetadata.embeds) {
    embeds = cachedMetadata.embeds.map(e => e.link);
  }

  let allLinks = [...links, ...embeds];
  console.log(allLinks);
  // 获取所有文件和链接文件路径
  let linkFilePaths = [];
  for (let i = 0; i < allLinks.length; i++) {
    const link = allLinks[i];
    const filePath = getFilePath(files, link);
    if (filePath) {
      linkFilePaths.push(filePath);
    }
  };
  console.log(linkFilePaths);
  // 检查链接文件是否在同一文件夹中
  const activefile = app.workspace.getActiveFile();
  console.log(activefile);
  // 筛选出附件
  const attachmentTypes = ['png', "jpg", 'jpeg', 'svg', 'gif', 'webp', 'mp4'];
  const attachments = linkFilePaths.filter(link => attachmentTypes.some(type => link.endsWith('.' + type)));
  // 移动文件到附件文件夹
  const attachmentFolder = await app.vault.config.attachmentFolderPath;
  const attachmentDateFormat = "YYYYMMDDhhmmssSSS";
  for (let i = 0; i < attachments.length; i++) {
    const oldFilePath = attachments[i];
    console.log(`oldFilePath:${oldFilePath}`);
    const ctime = await app.vault.getAbstractFileByPath(oldFilePath).stat["ctime"];
    const newFilePath = path.join(attachmentFolder,
      `File-${moment(ctime).format(attachmentDateFormat)}${path.extname(oldFilePath).toLowerCase()}`);
    console.log(newFilePath);
    // 检查文件名是否符合格式
    const regex = new RegExp(`File-\\d{17}${path.extname(oldFilePath).toLowerCase()}`);
    if (regex.test(path.basename(oldFilePath))) {
      console.log(`文件名已符合格式,跳过: ${oldFilePath}`);
      continue;
    }
    await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFilePath), newFilePath);
  }
  new Notice("✅已重命名");
};

// 获取文件路径函数
function getFilePath(files, baseName) {
  let files2 = files.filter(f => path.basename(f.path).replace(".md", "") === path.basename(baseName).replace(".md", ""));
  let filePath = files2.map((f) => f.path);
  return filePath[0];
}

function matchSelectionEmbed(text) {
  const regex = /\[\[?([^\]]*?)(\|.*)?\]\]?\(?([^)\n]*)\)?/;
  const matches = text.match(regex);
  if (!matches) return;
  if (matches[3]) return decodeURIComponent(matches[3]);
  if (matches[1]) return decodeURIComponent(matches[1]);
};
1 个赞

这个又是很漂亮!!!