fix(documents): key imports by source instance, keep a token on its origin, and harden the import path

- Source keys carry the normalised origin of the Paperless-ngx or Papra
  they came from (source_instance on the document, the alias and the
  purge ledger; unique NULLS NOT DISTINCT). A key stored without one
  (v1.39.2 imports) matches any instance of its system, and a lookup
  without one matches any stored instance. The upload route, the lookup
  route and the import script accept or send it; backups carry it.
- Saving a connection reuses the stored token only for the same origin;
  a new origin needs the token again (422 tokenRequired).
- Document ids are checked against what each system issues (Paperless
  digits, Papra [A-Za-z0-9_-], never . or ..) before any URL is built;
  the Papra organization id likewise.
- Paperless API 9 filters dates on created__gte / created__lte, in the
  picker and the script. A search page the local re-check empties reads
  on, up to five source pages. Tags are read past the first 250.
- The import's key pre-check uses the lookup allowance, a failed fetch
  no longer charges the upload allowance, and one import per key runs at
  a time under an advisory lock, so a double click downloads once.
- Reading and deleting one's own connection works with the documents
  module off and the operator list unset.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 14:20:17 +02:00
1 parent 6029c7c30d
commit 1eac05b454
24 files changed
+886 -209

No files matched your search

@@ -424,6 +424,7 @@ describe("import-documents.mjs — Paperless-ngx", () => {
kind: "LAB_RESULT",
sourceSystem: "PAPERLESS",
sourceId: "1",
sourceInstance: new URL(pl.url).origin,
aiRead: "defer",
},
});
@@ -664,6 +665,7 @@ describe("import-documents.mjs — Papra", () => {
documentDate: "2019-06-01",
sourceSystem: "PAPRA",
sourceId: "doc_a",
sourceInstance: new URL(pp.url).origin,
});
// --ai-read: no deferral field at all.
expect(sent[0].aiRead).toBeUndefined();
+8 -1
View File
@@ -301,6 +301,7 @@ function paperless(options) {
return {
system: "PAPERLESS",
instance: new URL(base).origin,
label,
async *documents() {
const query = new URLSearchParams({
@@ -324,7 +325,8 @@ function paperless(options) {
tagId = found[0].id;
query.set("tags__id__all", String(tagId));
}
if (options.since) query.set("created__date__gte", options.since);
// API version 9 made `created` a plain date: filter on it directly.
if (options.since) query.set("created__gte", options.since);
let typeNames = new Map();
if (options.kindMap.size > 0) {
@@ -399,6 +401,7 @@ function papra(options) {
return {
system: "PAPRA",
instance: new URL(options.baseUrl).origin,
label,
async *documents() {
let tagId = null;
@@ -526,6 +529,7 @@ function keyQuery(source, doc) {
return new URLSearchParams({
sourceSystem: source.system,
sourceId: doc.sourceId.slice(0, SOURCE_ID_MAX),
sourceInstance: source.instance,
});
}
@@ -617,6 +621,9 @@ async function upload(options, source, doc, file) {
if (!options.aiRead) form.append("aiRead", "defer");
form.append("sourceSystem", source.system);
form.append("sourceId", doc.sourceId.slice(0, SOURCE_ID_MAX));
// Which Paperless-ngx or Papra this id belongs to: the same id in two
// instances is two documents. HealthLog 1.39.2 ignores the field.
form.append("sourceInstance", source.instance);
return form;
},
);