Apr 8, 2026 2.0 KB typescript v4
1import { exec } from "child_process";
2import { Platform } from "obsidian";
3
4const XATTR_PREFIX = "com.sublimated.";
5const XATTR_SUFFIX = "#PS";
6
7export interface FileMetadata {
8 version: string | null;
9 commit: string | null;
10 url: string | null;
11 contentHash: string | null;
12 id: string | null;
13 createdBy: string | null;
14 modifiedBy: string | null;
15 modifiedByType: string | null;
16 syncStatus: string | null;
17 space: string | null;
18 branch: string | null;
19 modifiedAt: string | null;
20 visibility: string | null;
21}
22
23const ATTRS: (keyof FileMetadata)[] = [
24 "version",
25 "commit",
26 "url",
27 "contentHash",
28 "id",
29 "createdBy",
30 "modifiedBy",
31 "modifiedByType",
32 "syncStatus",
33 "space",
34 "branch",
35 "modifiedAt",
36 "visibility",
37];
38
39function attrToXattrName(attr: string): string {
40 // camelCase → kebab-case: contentHash → content-hash
41 const kebab = attr.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
42 return `${XATTR_PREFIX}${kebab}${XATTR_SUFFIX}`;
43}
44
45function readXattr(path: string, xattrName: string): Promise<string | null> {
46 return new Promise((resolve) => {
47 exec(
48 `xattr -p ${JSON.stringify(xattrName)} ${JSON.stringify(path)}`,
49 { timeout: 2000 },
50 (err, stdout) => {
51 if (err) {
52 resolve(null);
53 return;
54 }
55 resolve(stdout.trim() || null);
56 }
57 );
58 });
59}
60
61export async function readFileMetadata(
62 path: string
63): Promise<FileMetadata | null> {
64 if (!Platform.isMacOS) return null;
65
66 const results = await Promise.all(
67 ATTRS.map((attr) => readXattr(path, attrToXattrName(attr)))
68 );
69
70 // If no xattrs found at all, this isn't a sublimated file
71 if (results.every((r) => r === null)) return null;
72
73 const meta: FileMetadata = {
74 version: null, commit: null, url: null, contentHash: null,
75 id: null, createdBy: null, modifiedBy: null, modifiedByType: null,
76 syncStatus: null, space: null, branch: null, modifiedAt: null, visibility: null,
77 };
78 ATTRS.forEach((attr, i) => {
79 meta[attr] = results[i];
80 });
81 return meta;
82}
83
84export function isMacOS(): boolean {
85 return Platform.isMacOS;
86}