This commit is contained in:
Ludwig Lehnert
2026-10-03 15:43:12 +00:00
parent 9944f2be1f
commit 2b23422fa3
9 changed files with 130 additions and 14 deletions
+5
View File
@@ -300,6 +300,11 @@ def process_next(conn, prefer_ocr=False, stop=None):
if row is None:
worker_state(conn, 'idle')
return False
if documents.is_excluded_filename(row['name']):
conn.execute('DELETE FROM documents WHERE id=?', (row['id'],))
conn.commit()
worker_state(conn, 'idle')
return True
source = conn.execute('SELECT * FROM document_sources WHERE id=?', (row['source_id'],)).fetchone()
if source is None:
return False
+17 -6
View File
@@ -25,6 +25,11 @@ except ImportError:
SEARCH_ROOT = os.getenv('DOCUMENT_STATE_ROOT', os.path.join(os.getenv('STATE_ROOT', '/state'), 'documents'))
SEARCH_DB = os.path.join(SEARCH_ROOT, 'search.db')
MAX_TEXT = 2 * 1024 * 1024
SEARCHABLE_NAME_SQL = "lower({name}) <> 'thumbs.db' AND {name} NOT GLOB '~$*'"
def is_excluded_filename(name):
return name.casefold() == 'thumbs.db' or name.startswith('~$')
@@ -83,17 +88,20 @@ def ensure_schema(conn):
# Upgrade the derived index only: keep catalog IDs, permissions, previews
# and originals, but remove content and jobs for excluded file types.
if conn.execute('PRAGMA user_version').fetchone()[0] < 1:
if conn.execute('PRAGMA user_version').fetchone()[0] < 2:
with conn:
conn.execute('BEGIN IMMEDIATE')
if conn.execute('PRAGMA user_version').fetchone()[0] < 1:
version = conn.execute('PRAGMA user_version').fetchone()[0]
if version < 1:
content = sorted(CONTENT_SUFFIXES)
images = sorted(IMAGE_SUFFIXES)
conn.execute(f"""UPDATE documents SET body='',pages=0,attempts=0,retry_at=0,
state=CASE WHEN extension IN ({','.join('?' for _ in images)}) AND preview=''
THEN 'pending' ELSE 'name-only' END
WHERE extension NOT IN ({','.join('?' for _ in content)})""", [*images,*content])
conn.execute('PRAGMA user_version=1')
if version < 2:
conn.execute(f"DELETE FROM documents WHERE NOT ({SEARCHABLE_NAME_SQL.format(name='name')})")
conn.execute('PRAGMA user_version=2')
def worker_paused(conn):
@@ -243,6 +251,10 @@ def allowed_sources(conn, identity):
def discover_file(conn, source, relative, seen=0):
name = relative.rsplit('/', 1)[-1]
if is_excluded_filename(name):
conn.execute('DELETE FROM documents WHERE source_id=? AND path=?', (source['id'], relative))
return
try:
with open_file(source['root'], relative) as (_, info):
stamp = fingerprint(info)
@@ -254,7 +266,6 @@ def discover_file(conn, source, relative, seen=0):
if seen:
conn.execute('UPDATE documents SET seen=? WHERE source_id=? AND path=?', (seen, source['id'], relative))
return
name = relative.rsplit('/', 1)[-1]
extension = os.path.splitext(name)[1].lower()
state = 'pending' if extension in CONTENT_SUFFIXES | IMAGE_SUFFIXES else 'name-only'
conn.execute('''INSERT INTO documents(id,source_id,path,name,extension,size,modified,fingerprint,seen,state)
@@ -348,7 +359,7 @@ def private_readable(source, relative, uid):
def current_document(conn, document_id, identity):
sources = allowed_sources(conn, identity)
row = conn.execute('SELECT * FROM documents WHERE id=?', (document_id,)).fetchone()
if row is None or row['source_id'] not in sources:
if row is None or row['source_id'] not in sources or is_excluded_filename(row['name']):
raise FileNotFoundError('Document not found')
source = sources[row['source_id']]
if not private_readable(source, row['path'], identity.get('uid')):
@@ -397,7 +408,7 @@ def search(conn, identity, params):
for row in conn.execute(f"SELECT id,source_id,path FROM documents WHERE source_id IN ({placeholders}) AND source_id LIKE 'private:%'", ids).fetchall():
if private_readable(selected[row['source_id']], row['path'], identity.get('uid')):
conn.execute('INSERT INTO readable_private VALUES(?)', (row['id'],))
private_condition = "(d.source_id NOT LIKE 'private:%' OR EXISTS (SELECT 1 FROM readable_private r WHERE r.id=d.id))"
private_condition = SEARCHABLE_NAME_SQL.format(name='d.name') + " AND (d.source_id NOT LIKE 'private:%' OR EXISTS (SELECT 1 FROM readable_private r WHERE r.id=d.id))"
conditions = [f'd.source_id IN ({placeholders})', private_condition]
values = ids[:]
from_sql = 'documents d'
+1 -3
View File
@@ -2,7 +2,6 @@
const $ = selector => document.querySelector(selector);
const esc = value => String(value ?? "").replace(/[&<>'"]/g, char => ({"&":"&amp;","<":"&lt;",">":"&gt;","'":"&#39;",'"':"&quot;"}[char]));
const snippet = value => esc(value).replace(/\x01/g, "<mark>").replace(/\x02/g, "</mark>");
const sizeLabel = value => {
const units = ["B", "kB", "MB", "GB", "TB"];
let amount = Number(value), index = 0;
@@ -18,7 +17,6 @@ function icon(name) {
};
return `<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${paths[name]}</svg>`;
}
const statusLabel = row => ({pending:"Text wird erfasst…",ocr:row.attempts ? "Texterkennung wird erneut versucht…" : "Texterkennung läuft…",failed:"Text konnte noch nicht erfasst werden.","name-only":"Suche nach Dateinamen verfügbar."}[row.state] || "");
let session = null, offset = 0, listMode = false, timer, debounce, request, serial = 0, lastResult = "", activeDocument = null, initialFilters = null;
const pageSize = 40;
@@ -99,7 +97,7 @@ async function refresh(userAction = true) {
$("#documents").innerHTML = result.items.length ? result.items.map(row => `<article class="document-card">
<button type="button" class="document-open" data-document="${esc(row.id)}" aria-label="Öffnen: ${esc(row.name)}">
<div class="document-thumbnail">${row.hasPreview ? `<img src="/api/documents/${row.id}/preview?v=${row.version}" loading="lazy" alt="">` : `<div class="document-placeholder">${icon("file")}<span>${esc((row.extension || "Datei").toUpperCase())}</span></div>`}</div>
<div class="document-description"><h2 title="${esc(row.name)}">${esc(row.name)}</h2><p class="document-path" title="${esc(row.path)}">${esc(row.path)}</p><p class="document-excerpt">${snippet(row.snippet || statusLabel(row))}</p></div>
<div class="document-description"><h2 title="${esc(row.name)}">${esc(row.name)}</h2><p class="document-path" title="${esc(row.path)}">${esc(row.path)}</p></div>
</button><footer><span title="${esc(row.source)}">${esc(row.kind === "private" ? "Private" : row.source)} · ${sizeLabel(row.size)}<small>${modifiedLabel(row.modified)} UTC</small></span><a class="button icon-button" href="/api/documents/${row.id}/download" download="${esc(row.name)}" aria-label="Herunterladen: ${esc(row.name)}" title="Herunterladen">${icon("download")}</a></footer>
</article>`).join("") : '<div class="portal-empty"><h2>Keine Dateien gefunden</h2><p>Suchbegriff oder Filter ändern.</p></div>';
}
+2 -3
View File
@@ -6,6 +6,7 @@
* { box-sizing: border-box; }
[hidden] { display: none !important; }
body { margin: 0; min-height: 100vh; }
html:has(#document-dialog[open]), body:has(#document-dialog[open]) { overflow: hidden; overscroll-behavior: none; }
a { color: #0645ad; }
button, input, select { font: inherit; }
button, .button { display: inline-block; padding: .4rem .7rem; border: 1px solid #777; color: #111; background: #eee; cursor: pointer; text-decoration: none; white-space: nowrap; }
@@ -192,7 +193,6 @@ progress { width: 100%; }
.document-description { padding: .8rem; }
.document-description h2 { font-size: 1rem; margin: 0 0 .3rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.document-path { font-size: .8rem; color: #666; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.document-excerpt { height: 3.6em; overflow: hidden; font-size: .85rem; margin: .6rem 0 0; overflow-wrap: anywhere; }
mark { background: #fff0a0; color: #111; }
.document-card footer { display: flex; justify-content: space-between; align-items: center; gap: .5rem; padding: .6rem .8rem; border-top: 1px solid #ccc; }
.document-card footer > span { min-width: 0; font-size: .8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -203,7 +203,6 @@ mark { background: #fff0a0; color: #111; }
.document-list .document-thumbnail { width: 80px; height: 100px; flex-shrink: 0; border: 0; border-right: 1px solid #ccc; }
.document-list .document-description { min-width: 0; }
.document-list footer { flex-shrink: 0; border: 0; }
.document-list .document-excerpt { height: 1.4em; }
.portal-empty { grid-column: 1/-1; padding: 2rem; border: 1px solid #aaa; text-align: center; color: #666; }
.portal-empty h2 { color: #111; }
.portal-pagination { display: flex; align-items: center; justify-content: center; gap: 1rem; margin-top: 1.5rem; }
@@ -218,7 +217,7 @@ mark { background: #fff0a0; color: #111; }
.document-detail { height: calc(100% - 95px); min-height: 0; }
.document-preview { padding: 1rem; overflow: auto; background: #f5f5f5; }
.document-preview img { display: block; width: 100%; height: auto; }
.document-dialog.pdf-dialog { width: 100vw; height: 100dvh; max-width: 100vw; max-height: 100dvh; margin: 0; border: 0; }
.document-dialog.pdf-dialog { width: 100vw; height: 100dvh; max-width: 100vw; max-height: 100dvh; margin: 0; border: 0; overflow: hidden; }
.document-dialog.pdf-dialog[open] { display: flex; flex-direction: column; }
.pdf-dialog .document-dialog-head { flex-shrink: 0; padding: .6rem 1rem; }
.pdf-dialog .document-detail { flex: 1; height: auto; overflow: hidden; }