zotkit 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zotkit
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Headless Zotero library management: Web API CRUD plus direct WebDAV attachment upload/download — no desktop app required
5
5
  Author: Shawn
6
6
  License-Expression: MIT
@@ -22,12 +22,14 @@ Dynamic: license-file
22
22
 
23
23
  **Headless Zotero library management — no desktop app required.**
24
24
 
25
- zotkit is a Python library + CLI that manages a Zotero library entirely through the
26
- [Zotero Web API](https://www.zotero.org/support/dev/web_api/v3/start): search, create,
27
- tag, and organize items from a terminal or a server. Uniquely, if your attachments sync
28
- to a **personal WebDAV server**, zotkit can **upload and download the files themselves**
29
- by speaking Zotero's WebDAV storage format directly the one capability the Web API
30
- does not provide, and (as far as we know) not offered by any other headless tool.
25
+ "Headless" simply means zotkit never needs the Zotero app (or any window) open: it is a
26
+ Python library + CLI that talks straight to the
27
+ [Zotero Web API](https://www.zotero.org/support/dev/web_api/v3/start), so you can
28
+ search, create, tag, and organize items from any terminal macOS, Windows, or Linux,
29
+ your laptop or a remote server. Uniquely, if your attachments sync to a **personal
30
+ WebDAV server**, zotkit can **upload and download the files themselves** by speaking
31
+ Zotero's WebDAV storage format directly — the one capability the Web API does not
32
+ provide, and (as far as we know) not offered by any other headless tool.
31
33
 
32
34
  Built for servers, scripts, and **LLM agents**: every write is dry-run by default,
33
35
  batched, and version-checked, and you can define a tag taxonomy that is *enforced in
@@ -39,11 +41,15 @@ code* so an agent (or a tired human) can't pollute your library with inconsisten
39
41
  |---|---|---|---|
40
42
  | Works headless (server, SSH, CI) | ❌ | ✅ read-mostly | ✅ |
41
43
  | Write items/tags/collections | ✅ | ⚠️ usually needs the desktop app running | ✅ |
44
+ | Attachment files (Zotero Storage) | ✅ | ⚠️ some | ✅ upload + download |
42
45
  | Attachment files on **WebDAV** | ✅ | ⚠️ download at best | ✅ **upload + download** |
43
46
  | Tag conventions enforced in code | ❌ | ❌ | ✅ optional `conventions.toml` |
44
47
 
45
48
  ## Install
46
49
 
50
+ Pure Python (3.11+), no platform-specific bits — the same package works on macOS,
51
+ Windows, and Linux:
52
+
47
53
  ```bash
48
54
  pipx install zotkit # or: uv tool install zotkit / pip install zotkit
49
55
  uvx zotkit --help # …or try it without installing anything
@@ -60,6 +66,9 @@ Copy [`.env.example`](.env.example) to `./.env`, `~/.config/zotkit/env`, or any
60
66
  - **WebDAV** (only for `attach`/`fetch`): copy the exact values from the Zotero desktop
61
67
  app on any of your machines — **Settings → Sync → File Syncing** — and append
62
68
  `/zotero/` to the URL (the desktop does this implicitly).
69
+ - **Using Zotero Storage instead of WebDAV?** Just leave the `WEBDAV_*` lines out —
70
+ `attach`/`fetch` automatically use Zotero Storage through the Web API's upload/download
71
+ endpoints instead. The storage mode is detected from your `.env`, nothing to configure.
63
72
 
64
73
  Optionally, copy [`conventions.example.toml`](conventions.example.toml) to
65
74
  `conventions.toml` next to your `.env` to define a namespaced tag taxonomy
@@ -114,6 +123,22 @@ z.backup()
114
123
  `z.z` is the underlying [pyzotero](https://github.com/urschrei/pyzotero) client for
115
124
  anything not wrapped.
116
125
 
126
+ ## Using zotkit with AI agents
127
+
128
+ zotkit is designed to be driven by coding agents (Claude Code and similar): dry-run
129
+ defaults, code-enforced tag conventions, and a ready-made **Claude Code skill** in
130
+ [`skills/zotkit/`](skills/zotkit/SKILL.md) — copy it to `~/.claude/skills/zotkit/` and
131
+ any Claude session can search, file, and attach papers for you while respecting your
132
+ taxonomy. (An MCP server is planned.)
133
+
134
+ Want to clean up a messy library, not just maintain one? The battle-tested method —
135
+ taxonomy design, parallel read-only analysis, serial reviewed writes — is written up in
136
+ [`docs/organizing-with-agents.md`](docs/organizing-with-agents.md).
137
+
138
+ ```bash
139
+ mkdir -p ~/.claude/skills && cp -r skills/zotkit ~/.claude/skills/
140
+ ```
141
+
117
142
  ## Safety model
118
143
 
119
144
  - `create` is **dry-run by default**; `--apply` to execute.
@@ -125,11 +150,12 @@ anything not wrapped.
125
150
 
126
151
  ## How WebDAV attachments work
127
152
 
128
- Zotero's WebDAV storage format is undocumented but simple: each attachment item `K` is
129
- stored as `K.zip` (the file, zipped) plus `K.prop` (its md5 + mtime). zotkit creates the
130
- attachment item via the Web API and PUTs both objects directly — after which every
131
- desktop client syncs the file down normally. Details in
132
- [`docs/webdav-format.md`](docs/webdav-format.md).
153
+ (With Zotero Storage, zotkit simply uses the Web API's official file endpoints this
154
+ section is about the WebDAV mode.) Zotero's WebDAV storage format is undocumented but
155
+ simple: each attachment item `K` is stored as `K.zip` (the file, zipped) plus `K.prop`
156
+ (its md5 + mtime). zotkit creates the attachment item via the Web API and PUTs both
157
+ objects directly — after which every desktop client syncs the file down normally.
158
+ Details in [`docs/webdav-format.md`](docs/webdav-format.md).
133
159
 
134
160
  The format was determined by interoperability inspection of the author's own library.
135
161
  This project is not affiliated with or endorsed by Zotero.
@@ -2,12 +2,14 @@
2
2
 
3
3
  **Headless Zotero library management — no desktop app required.**
4
4
 
5
- zotkit is a Python library + CLI that manages a Zotero library entirely through the
6
- [Zotero Web API](https://www.zotero.org/support/dev/web_api/v3/start): search, create,
7
- tag, and organize items from a terminal or a server. Uniquely, if your attachments sync
8
- to a **personal WebDAV server**, zotkit can **upload and download the files themselves**
9
- by speaking Zotero's WebDAV storage format directly the one capability the Web API
10
- does not provide, and (as far as we know) not offered by any other headless tool.
5
+ "Headless" simply means zotkit never needs the Zotero app (or any window) open: it is a
6
+ Python library + CLI that talks straight to the
7
+ [Zotero Web API](https://www.zotero.org/support/dev/web_api/v3/start), so you can
8
+ search, create, tag, and organize items from any terminal macOS, Windows, or Linux,
9
+ your laptop or a remote server. Uniquely, if your attachments sync to a **personal
10
+ WebDAV server**, zotkit can **upload and download the files themselves** by speaking
11
+ Zotero's WebDAV storage format directly — the one capability the Web API does not
12
+ provide, and (as far as we know) not offered by any other headless tool.
11
13
 
12
14
  Built for servers, scripts, and **LLM agents**: every write is dry-run by default,
13
15
  batched, and version-checked, and you can define a tag taxonomy that is *enforced in
@@ -19,11 +21,15 @@ code* so an agent (or a tired human) can't pollute your library with inconsisten
19
21
  |---|---|---|---|
20
22
  | Works headless (server, SSH, CI) | ❌ | ✅ read-mostly | ✅ |
21
23
  | Write items/tags/collections | ✅ | ⚠️ usually needs the desktop app running | ✅ |
24
+ | Attachment files (Zotero Storage) | ✅ | ⚠️ some | ✅ upload + download |
22
25
  | Attachment files on **WebDAV** | ✅ | ⚠️ download at best | ✅ **upload + download** |
23
26
  | Tag conventions enforced in code | ❌ | ❌ | ✅ optional `conventions.toml` |
24
27
 
25
28
  ## Install
26
29
 
30
+ Pure Python (3.11+), no platform-specific bits — the same package works on macOS,
31
+ Windows, and Linux:
32
+
27
33
  ```bash
28
34
  pipx install zotkit # or: uv tool install zotkit / pip install zotkit
29
35
  uvx zotkit --help # …or try it without installing anything
@@ -40,6 +46,9 @@ Copy [`.env.example`](.env.example) to `./.env`, `~/.config/zotkit/env`, or any
40
46
  - **WebDAV** (only for `attach`/`fetch`): copy the exact values from the Zotero desktop
41
47
  app on any of your machines — **Settings → Sync → File Syncing** — and append
42
48
  `/zotero/` to the URL (the desktop does this implicitly).
49
+ - **Using Zotero Storage instead of WebDAV?** Just leave the `WEBDAV_*` lines out —
50
+ `attach`/`fetch` automatically use Zotero Storage through the Web API's upload/download
51
+ endpoints instead. The storage mode is detected from your `.env`, nothing to configure.
43
52
 
44
53
  Optionally, copy [`conventions.example.toml`](conventions.example.toml) to
45
54
  `conventions.toml` next to your `.env` to define a namespaced tag taxonomy
@@ -94,6 +103,22 @@ z.backup()
94
103
  `z.z` is the underlying [pyzotero](https://github.com/urschrei/pyzotero) client for
95
104
  anything not wrapped.
96
105
 
106
+ ## Using zotkit with AI agents
107
+
108
+ zotkit is designed to be driven by coding agents (Claude Code and similar): dry-run
109
+ defaults, code-enforced tag conventions, and a ready-made **Claude Code skill** in
110
+ [`skills/zotkit/`](skills/zotkit/SKILL.md) — copy it to `~/.claude/skills/zotkit/` and
111
+ any Claude session can search, file, and attach papers for you while respecting your
112
+ taxonomy. (An MCP server is planned.)
113
+
114
+ Want to clean up a messy library, not just maintain one? The battle-tested method —
115
+ taxonomy design, parallel read-only analysis, serial reviewed writes — is written up in
116
+ [`docs/organizing-with-agents.md`](docs/organizing-with-agents.md).
117
+
118
+ ```bash
119
+ mkdir -p ~/.claude/skills && cp -r skills/zotkit ~/.claude/skills/
120
+ ```
121
+
97
122
  ## Safety model
98
123
 
99
124
  - `create` is **dry-run by default**; `--apply` to execute.
@@ -105,11 +130,12 @@ anything not wrapped.
105
130
 
106
131
  ## How WebDAV attachments work
107
132
 
108
- Zotero's WebDAV storage format is undocumented but simple: each attachment item `K` is
109
- stored as `K.zip` (the file, zipped) plus `K.prop` (its md5 + mtime). zotkit creates the
110
- attachment item via the Web API and PUTs both objects directly — after which every
111
- desktop client syncs the file down normally. Details in
112
- [`docs/webdav-format.md`](docs/webdav-format.md).
133
+ (With Zotero Storage, zotkit simply uses the Web API's official file endpoints this
134
+ section is about the WebDAV mode.) Zotero's WebDAV storage format is undocumented but
135
+ simple: each attachment item `K` is stored as `K.zip` (the file, zipped) plus `K.prop`
136
+ (its md5 + mtime). zotkit creates the attachment item via the Web API and PUTs both
137
+ objects directly — after which every desktop client syncs the file down normally.
138
+ Details in [`docs/webdav-format.md`](docs/webdav-format.md).
113
139
 
114
140
  The format was determined by interoperability inspection of the author's own library.
115
141
  This project is not affiliated with or endorsed by Zotero.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zotkit"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Headless Zotero library management: Web API CRUD plus direct WebDAV attachment upload/download — no desktop app required"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,4 +2,4 @@
2
2
  from .core import Conventions, TagConventionError, Zot, lint_tags, load_conventions
3
3
 
4
4
  __all__ = ["Zot", "lint_tags", "load_conventions", "Conventions", "TagConventionError"]
5
- __version__ = "0.1.0"
5
+ __version__ = "0.2.0"
@@ -76,7 +76,7 @@ def main(argv=None):
76
76
  _print_items(zot.find(a.title, a.tag, a.collection))
77
77
 
78
78
  elif a.cmd == "create":
79
- data = json.load(open(a.file))
79
+ data = json.load(open(a.file, encoding="utf-8"))
80
80
  items = data["items"] if isinstance(data, dict) else data
81
81
  if not a.apply:
82
82
  for d in items:
@@ -89,7 +89,7 @@ def main(argv=None):
89
89
  created = zot.create_items(items, dedup=not a.no_dedup, strict_tags=not a.loose_tags)
90
90
  new = [c for c in created if c.get("key")]
91
91
  out = Path(a.file).with_suffix(".created.json")
92
- json.dump(new, open(out, "w"), ensure_ascii=False, indent=2)
92
+ json.dump(new, open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
93
93
  print(f"created {len(new)} (skipped {len(created)-len(new)} dup) -> {out}")
94
94
  if any(c.get("file_path") for c in new):
95
95
  print(f"attach PDFs: zotkit attach --from {out} --all")
@@ -98,7 +98,7 @@ def main(argv=None):
98
98
  if a.key and a.pdf:
99
99
  print(zot.attach(a.key, a.pdf))
100
100
  elif a.src:
101
- rows = json.load(open(a.src))
101
+ rows = json.load(open(a.src, encoding="utf-8"))
102
102
  done = 0
103
103
  for m in rows:
104
104
  if not m.get("file_path") or not Path(m["file_path"]).exists():
@@ -50,7 +50,7 @@ def load_env(path: str | os.PathLike | None = None) -> dict[str, str]:
50
50
  if not p.is_file():
51
51
  continue
52
52
  env: dict[str, str] = {}
53
- for line in p.read_text().splitlines():
53
+ for line in p.read_text(encoding="utf-8").splitlines():
54
54
  line = line.strip()
55
55
  if line and not line.startswith("#") and "=" in line:
56
56
  k, v = line.split("=", 1)
@@ -97,7 +97,7 @@ def load_conventions(env_dir: str | os.PathLike | None = None) -> Conventions |
97
97
  cands.append(Path.home() / ".config" / "zotkit" / "conventions.toml")
98
98
  for p in cands:
99
99
  if p.is_file():
100
- return Conventions(tomllib.loads(p.read_text()))
100
+ return Conventions(tomllib.loads(p.read_text(encoding="utf-8")))
101
101
  return None
102
102
 
103
103
 
@@ -305,8 +305,19 @@ class Zot:
305
305
  "need WEBDAV_URL/WEBDAV_USER/WEBDAV_PASS") from None
306
306
 
307
307
  def attach(self, item_key: str, file_path: str | os.PathLike) -> dict:
308
- """Attach a local file to an item; bytes go to WebDAV (<key>.zip + .prop)."""
308
+ """Attach a local file to an item. Bytes go to WebDAV (<key>.zip + .prop)
309
+ when WEBDAV_* is configured, otherwise to Zotero Storage via the Web API."""
309
310
  f = Path(file_path)
311
+ if "WEBDAV_URL" not in self.env:
312
+ resp = self.z.attachment_simple([str(f)], item_key)
313
+ done = (resp.get("success") or []) + (resp.get("unchanged") or [])
314
+ if not done:
315
+ raise RuntimeError(f"Zotero Storage upload failed: {resp}")
316
+ entry = done[0]
317
+ key = ""
318
+ if isinstance(entry, dict):
319
+ key = entry.get("key") or entry.get("data", {}).get("key", "")
320
+ return {"attachment_key": key, "filename": f.name, "storage": "zotero"}
310
321
  filename, md5 = f.name, _md5_file(f)
311
322
  mtime = int(os.path.getmtime(f) * 1000)
312
323
  ctype = mimetypes.guess_type(filename)[0] or "application/octet-stream"
@@ -329,28 +340,36 @@ class Zot:
329
340
  ok = r1.status_code in (200, 201, 204) and r2.status_code in (200, 201, 204)
330
341
  if not ok:
331
342
  raise RuntimeError(f"WebDAV PUT failed: zip={r1.status_code} prop={r2.status_code}")
332
- return {"attachment_key": key, "filename": filename, "webdav_ok": ok}
343
+ return {"attachment_key": key, "filename": filename, "storage": "webdav"}
333
344
 
334
345
  def has_attachment(self, item_key: str) -> bool:
335
346
  return any(k["data"].get("itemType") == "attachment"
336
347
  for k in self.z.children(item_key))
337
348
 
338
349
  def fetch(self, item_key: str, out_dir: str | os.PathLike = "downloads") -> list[Path]:
339
- """Download an item's attachment files from WebDAV; returns saved paths."""
350
+ """Download an item's attachment files (from WebDAV when configured,
351
+ otherwise from Zotero Storage); returns saved paths."""
340
352
  out = Path(out_dir)
341
353
  out.mkdir(parents=True, exist_ok=True)
354
+ saved: list[Path] = []
355
+ atts = [ch for ch in self.z.children(item_key)
356
+ if ch["data"].get("itemType") == "attachment" and ch["data"].get("filename")]
357
+ if "WEBDAV_URL" not in self.env:
358
+ for ch in atts:
359
+ try:
360
+ self.z.dump(ch["key"], ch["data"]["filename"], str(out))
361
+ saved.append(out / ch["data"]["filename"])
362
+ except Exception:
363
+ continue
364
+ return saved
342
365
  base, auth = self._webdav()
343
- saved = []
344
366
  with httpx.Client(auth=auth, timeout=120) as c:
345
- for ch in self.z.children(item_key):
346
- d = ch["data"]
347
- if d.get("itemType") != "attachment" or not d.get("filename"):
348
- continue
367
+ for ch in atts:
349
368
  r = c.get(f"{base}/{ch['key']}.zip")
350
369
  if r.status_code != 200:
351
370
  continue
352
371
  zipfile.ZipFile(io.BytesIO(r.content)).extractall(out)
353
- saved.append(out / d["filename"])
372
+ saved.append(out / ch["data"]["filename"])
354
373
  return saved
355
374
 
356
375
  # ---------- backup ----------
@@ -375,5 +394,5 @@ class Zot:
375
394
  out = Path(out_dir or (self.home / "backups"))
376
395
  out.mkdir(parents=True, exist_ok=True)
377
396
  p = out / f"zotero_backup_v{version}.json"
378
- p.write_text(json.dumps(backup, ensure_ascii=False, indent=2))
397
+ p.write_text(json.dumps(backup, ensure_ascii=False, indent=2), encoding="utf-8")
379
398
  return p
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zotkit
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Headless Zotero library management: Web API CRUD plus direct WebDAV attachment upload/download — no desktop app required
5
5
  Author: Shawn
6
6
  License-Expression: MIT
@@ -22,12 +22,14 @@ Dynamic: license-file
22
22
 
23
23
  **Headless Zotero library management — no desktop app required.**
24
24
 
25
- zotkit is a Python library + CLI that manages a Zotero library entirely through the
26
- [Zotero Web API](https://www.zotero.org/support/dev/web_api/v3/start): search, create,
27
- tag, and organize items from a terminal or a server. Uniquely, if your attachments sync
28
- to a **personal WebDAV server**, zotkit can **upload and download the files themselves**
29
- by speaking Zotero's WebDAV storage format directly the one capability the Web API
30
- does not provide, and (as far as we know) not offered by any other headless tool.
25
+ "Headless" simply means zotkit never needs the Zotero app (or any window) open: it is a
26
+ Python library + CLI that talks straight to the
27
+ [Zotero Web API](https://www.zotero.org/support/dev/web_api/v3/start), so you can
28
+ search, create, tag, and organize items from any terminal macOS, Windows, or Linux,
29
+ your laptop or a remote server. Uniquely, if your attachments sync to a **personal
30
+ WebDAV server**, zotkit can **upload and download the files themselves** by speaking
31
+ Zotero's WebDAV storage format directly — the one capability the Web API does not
32
+ provide, and (as far as we know) not offered by any other headless tool.
31
33
 
32
34
  Built for servers, scripts, and **LLM agents**: every write is dry-run by default,
33
35
  batched, and version-checked, and you can define a tag taxonomy that is *enforced in
@@ -39,11 +41,15 @@ code* so an agent (or a tired human) can't pollute your library with inconsisten
39
41
  |---|---|---|---|
40
42
  | Works headless (server, SSH, CI) | ❌ | ✅ read-mostly | ✅ |
41
43
  | Write items/tags/collections | ✅ | ⚠️ usually needs the desktop app running | ✅ |
44
+ | Attachment files (Zotero Storage) | ✅ | ⚠️ some | ✅ upload + download |
42
45
  | Attachment files on **WebDAV** | ✅ | ⚠️ download at best | ✅ **upload + download** |
43
46
  | Tag conventions enforced in code | ❌ | ❌ | ✅ optional `conventions.toml` |
44
47
 
45
48
  ## Install
46
49
 
50
+ Pure Python (3.11+), no platform-specific bits — the same package works on macOS,
51
+ Windows, and Linux:
52
+
47
53
  ```bash
48
54
  pipx install zotkit # or: uv tool install zotkit / pip install zotkit
49
55
  uvx zotkit --help # …or try it without installing anything
@@ -60,6 +66,9 @@ Copy [`.env.example`](.env.example) to `./.env`, `~/.config/zotkit/env`, or any
60
66
  - **WebDAV** (only for `attach`/`fetch`): copy the exact values from the Zotero desktop
61
67
  app on any of your machines — **Settings → Sync → File Syncing** — and append
62
68
  `/zotero/` to the URL (the desktop does this implicitly).
69
+ - **Using Zotero Storage instead of WebDAV?** Just leave the `WEBDAV_*` lines out —
70
+ `attach`/`fetch` automatically use Zotero Storage through the Web API's upload/download
71
+ endpoints instead. The storage mode is detected from your `.env`, nothing to configure.
63
72
 
64
73
  Optionally, copy [`conventions.example.toml`](conventions.example.toml) to
65
74
  `conventions.toml` next to your `.env` to define a namespaced tag taxonomy
@@ -114,6 +123,22 @@ z.backup()
114
123
  `z.z` is the underlying [pyzotero](https://github.com/urschrei/pyzotero) client for
115
124
  anything not wrapped.
116
125
 
126
+ ## Using zotkit with AI agents
127
+
128
+ zotkit is designed to be driven by coding agents (Claude Code and similar): dry-run
129
+ defaults, code-enforced tag conventions, and a ready-made **Claude Code skill** in
130
+ [`skills/zotkit/`](skills/zotkit/SKILL.md) — copy it to `~/.claude/skills/zotkit/` and
131
+ any Claude session can search, file, and attach papers for you while respecting your
132
+ taxonomy. (An MCP server is planned.)
133
+
134
+ Want to clean up a messy library, not just maintain one? The battle-tested method —
135
+ taxonomy design, parallel read-only analysis, serial reviewed writes — is written up in
136
+ [`docs/organizing-with-agents.md`](docs/organizing-with-agents.md).
137
+
138
+ ```bash
139
+ mkdir -p ~/.claude/skills && cp -r skills/zotkit ~/.claude/skills/
140
+ ```
141
+
117
142
  ## Safety model
118
143
 
119
144
  - `create` is **dry-run by default**; `--apply` to execute.
@@ -125,11 +150,12 @@ anything not wrapped.
125
150
 
126
151
  ## How WebDAV attachments work
127
152
 
128
- Zotero's WebDAV storage format is undocumented but simple: each attachment item `K` is
129
- stored as `K.zip` (the file, zipped) plus `K.prop` (its md5 + mtime). zotkit creates the
130
- attachment item via the Web API and PUTs both objects directly — after which every
131
- desktop client syncs the file down normally. Details in
132
- [`docs/webdav-format.md`](docs/webdav-format.md).
153
+ (With Zotero Storage, zotkit simply uses the Web API's official file endpoints this
154
+ section is about the WebDAV mode.) Zotero's WebDAV storage format is undocumented but
155
+ simple: each attachment item `K` is stored as `K.zip` (the file, zipped) plus `K.prop`
156
+ (its md5 + mtime). zotkit creates the attachment item via the Web API and PUTs both
157
+ objects directly — after which every desktop client syncs the file down normally.
158
+ Details in [`docs/webdav-format.md`](docs/webdav-format.md).
133
159
 
134
160
  The format was determined by interoperability inspection of the author's own library.
135
161
  This project is not affiliated with or endorsed by Zotero.
File without changes
File without changes