从零开始写一个极简版Hookmark

#文件管理 #DIY工具

1.Hookmark 最让我惊艳的点

在我们写笔记时,难免会引用到一些文件,以前的话,我都是用的文件的绝对路径的 file 协议链接。

操作流程就是:

  1. 先在 Finder 拷贝完整路径,例如:/Users/yutianran/Documents/关注圈 影响圈 知乎.jpg
  2. 再在 Chrome 地址栏中粘贴,重新拷贝它进行了空格和中文转码后的链接
  3. 最后在笔记工具中粘贴这个 file 协议的链接 file:///Users/yutianran/Documents/%E5%85%B3%E6%B3%A8%E5%9C%88%20%E5%BD%B1%E5%93%8D%E5%9C%88%20%E7%9F%A5%E4%B9%8E.jpg

但是这样就存在一个问题:如果被引用的文件改名了,或者被移动了位置,那我就很难再通过笔记直接访问到它。

而 Hookmark 最舒服的地方就是可以用快捷键快速创建一个 md 链接,例如:[关注圈 影响圈 知乎.jpg](hook://file/XQsipOANE?p=eXV0aWFucmFuL0RvY3VtZW50cw==&n=%E5%85%B3%E6%B3%A8%E5%9C%88%20%E5%BD%B1%E5%93%8D%E5%9C%88%20%E7%9F%A5%E4%B9%8E%2Ejpg),最神奇的是,之后这个文件,无论是改名还是移动,都可以通过这个链接访问到。

2.开始模仿 Hookmark

Hookmark 只能在 Mac 系统上使用,我仔细研究了 Mac 文件系统的替身、软连接、硬连接的区别以后,我觉得 Hookmark 应该主要就是用 Mac 系统的替身来实现的。

下面梳理一下我想要实现的流程: 1-先用 Mac 系统自带的 Automator 来获取当前选中的文件的路径 2-将路径传递给一个本地开启的 Web 服务,这里我准备用 Python 的 FastApi 实现 3-根据原始路径,在本机的固定目录下创建一个替身文件 4-为替身文件创建一个 file 协议链接,以后都可以通过替身文件去访问原始文件

针对上面的示例路径,我最后生成的链接是这样的:[关注圈 影响圈 知乎](file:///Users/yutianran/Documents/.link/%E5%85%B3%E6%B3%A8%E5%9C%88%20%E5%BD%B1%E5%93%8D%E5%9C%88%20%E7%9F%A5%E4%B9%8E.jpg-4f134d0946),同样不怕改名和移动

3-自定义 Automator

在 Automator 中新建文稿,选择文稿类型为:快速操作,这样方便我们在 Finder 下可以右键看到这个服务,Pasted image 20231016181412 不过每次要去快速操作里面找也挺麻烦的,所以可以去系统设置里面给它设置一个快捷键: 键盘-键盘快捷键-服务-文件和文件夹-create_link Pasted image 20231016184251

这个 create_link.workflow 文件路径在~/Library/Services/ Pasted image 20231016181310 workflow 本身其实啥也没干,就是获取文件路径参数,然后发起本地网络请求

1
2
3
4
5
file_path=$1
curl --location --request POST 'http://localhost:8002/' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--form 'operate="link"' \
--form 'resource="'"$file_path"'"'

5.自定义本地网络服务

这个本地网络服务同样啥也没干,就是接收文件路径参数,然后调用了 alias_util 工具类,最后将结果拷贝到系统剪切版

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
from fastapi import FastAPI, Response, Form, HTTPException, UploadFile, File
import uvicorn
import subprocess
import pyperclip
import os

import alias_util


app = FastAPI()


@app.get("/{operate}/{resource}")
def do_get(operate: str, resource: str):
    try:
        result = process(operate, resource)
        return Response(content=result, media_type="text/html")
    except BaseException as e:
        print(e)
        raise HTTPException(status_code=400, detail=str(e))


@app.post("/")
def do_post(operate: str = Form(...), resource: str = Form(...)):
    try:
        result = process(operate, resource)
        return Response(content=result, media_type="text/html")
    except BaseException as e:
        print(e)
        raise HTTPException(status_code=400, detail=str(e))


@app.post("/upload/")
async def upload(file: UploadFile = File(...)):
    return {"filename": file.filename}


# 核心的中转方法
def process(operate: str, resource: str):
    print(f"process operate: {operate} resource: {resource}")
    func_map = {
        "link": link,
        # "proxy":proxy,
        # "alias": alias,
        # "origin": origin,
        # "read": read_resource,
        # "open": open_resource,
    }
    if operate in func_map:
        return func_map[operate](resource)
    else:
        raise ServerException("Invalid Request")


class ServerException(Exception):
    pass


def execute_cmd(shell_command):
    print(f"shell_command: {shell_command}")
    process = subprocess.run(
        shell_command,
        shell=True,
        check=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    return_code = process.returncode
    output = process.stdout.decode("utf-8")
    error = process.stderr.decode("utf-8")
    print(f"return_code: {return_code}")
    print(f"output: {output}")
    print(f"error: {error}")
    return return_code, output, error


def link(resource):
    result = alias_util.create_alias(resource)
    # 拷贝到系统剪切版
    pyperclip.copy(result)
    return result
    # shell_command = f'sh /Users/yutianran/MyGithub/MyVSCode/test-fastapi/script/automator_link.sh "{resource}"'
    # print(f"shell_command: {shell_command}")
    # return_code, output, error = execute_cmd(shell_command)
    # if return_code == 0:
    #     pyperclip.copy(output)
    #     return output
    # else:
    #     raise ServerException(error)


def main():
    print("start main app_automator ...")
    print("http://localhost:8002/")
    uvicorn.run("app_automator:app", host="0.0.0.0", port=8002, reload=True)


if __name__ == "__main__":
    main()

4.核心的 alias_util 工具类

最终都是这个工具类在负重前行,核心点就是用 AppleScript 来创建替身文件了,用 ln mv 等命令行是不行的

这个 AppleScript 的 api 太难找了,我问了好多次 GPT 才总算试验出来了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/env python

import os
import sys
import urllib.parse
import hashlib
import subprocess


# 创建替身文件
def create_alias(file_path):
    print(f"file_path: {file_path}")
    file_name = os.path.basename(file_path)
    file_nam = os.path.splitext(file_name)[0]
    file_ext = os.path.splitext(file_name)[1]

    alias_folder_path = os.path.expanduser("~/Documents/.link/")
    print(f"alias_folder_path: {alias_folder_path}")

    # 如果alias_folder_path文件夹不存在就创建
    if not os.path.exists(alias_folder_path):
        os.makedirs(alias_folder_path)

    hash_value = hashlib.md5(file_path.encode()).hexdigest()
    alias_file_name = f"{file_name}-{hash_value[:10]}"
    print(f"alias_file_name: {alias_file_name}")

    alias_file_path = os.path.join(alias_folder_path, alias_file_name)

    # 定义要执行的AppleScript代码
    applescript_code = f"""
    tell application "Finder"
        if not (exists POSIX file "{alias_file_path}") then
            set alias_name to "{alias_file_name}" -- 替身文件名
            make new alias at POSIX file "{alias_folder_path}" to POSIX file "{file_path}" with properties {{name:alias_name}}
        end if
    end tell
    """
    # print(applescript_code)
    subprocess.run(["osascript", "-e", applescript_code], check=True)
    print(f"alias_file_path: {alias_file_path}")

    urlencode_name = urllib.parse.quote(alias_file_name)
    print(f"urlencode_name: {urlencode_name}")

    # 替身文件的file协议
    file_schema_link = f"file://{alias_folder_path}{urlencode_name}"
    return f"[{file_nam}]({file_schema_link})"


def main():
    link = create_alias(
        "/Users/yutianran/MyGithub/MyVSCode/test-fastapi/MyHookMark流程梳理.md"
    )
    print(f"mdlink: {link}")


if __name__ == "__main__":
    main()

5.其它补充

最开始我是直接调用 shell 脚本文件来实现 alias_util 工具类的,但是不知道为啥直接运行好好的,通过 Automator 来调用它就一直不行,无奈放弃

后来改为直接调用 python 脚本,也是同样的问题,都是单独测试脚本文件是可以的,一集成到 Automator 里面就不行

最后只好用本地网络服务做了一下中转

不过后来发现其实加一层网络服务,有 2 个好处

  1. 方便我们用 Apifox 做测试,比如 Automator 里面内嵌的 curl 指令就是用 Apifox 自动生成的
  2. 方便我们用 pm2 来管理这个服务和它的日志
1
2
3
4
5
pm2 start my-hookmark/app_automator.py --interpreter python3 #启动服务
pm2 list #查看服务列表
pm2 logs app_automator #查看服务日志
pm2 stop app_automator #停止服务
pm2 delete app_automator #删除服务

参考资料