fts (2)
This commit is contained in:
@@ -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
@@ -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
@@ -2,7 +2,6 @@
|
||||
|
||||
const $ = selector => document.querySelector(selector);
|
||||
const esc = value => String(value ?? "").replace(/[&<>'"]/g, char => ({"&":"&","<":"<",">":">","'":"'",'"':"""}[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
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user