Separate OnlyOffice async handling, add office_previews WS flag, framed worker protocol

- Move OnlyOffice conversion out of worker subprocesses into async event loop
  using httpx.AsyncClient with shared client and clean shutdown hook
- Add OOConversionManager with in-flight deduplication (asyncio.Future) and
  configurable concurrency limit (OO_MAX_CONCURRENT=2)
- Add 10s total timeout for office previews via asyncio.wait_for
- Return 503 when OnlyOffice is unavailable, 504 on timeout
- Remove office handling from worker dispatch(); workers now only do
  images, video, and PDFs
- Replace PNG tempfile bridge with framed binary input protocol on worker
  stdin: (json_size)(data_size)(json)(raw_data)
- Add process_image_buffer() for in-memory AVIF conversion via pyvips
- Add office_previews to WS server message, cached every 30s from OO
  availability check
- Frontend gates only office document previews on office_previews flag;
  images, video, PDFs remain unconditional
- Change default OnlyOffice port from 8080 to 8988
This commit is contained in:
Leo Vasanko
2026-05-01 03:42:03 +00:00
parent cb954db537
commit 49cb4a8255
8 changed files with 396 additions and 91 deletions
+72 -53
View File
@@ -20,6 +20,7 @@
<script setup lang="ts">
import { Play as PlayIcon, Spinner as SpinnerIcon } from '@/assets/svg'
import type { Doc } from '@/repositories/Document'
import { useMainStore } from '@/stores/main'
import { computed, ref } from 'vue'
const aud = ref<HTMLAudioElement | null>(null)
@@ -130,59 +131,77 @@ const audio = () => ['mp3', 'flac', 'ogg', 'aac'].includes(props.doc.ext)
const archive = () =>
['zip', 'tar', 'gz', 'bz2', 'xz', '7z', 'rar'].includes(props.doc.ext)
const showProgress = () => !props.doc.complete && (preview() || props.doc.img)
const preview = () =>
[
'bmp',
'ico',
'tif',
'tiff',
'heic',
'heif',
'pdf',
'epub',
'mobi',
// Documents
'doc',
'dot',
'docx',
'docm',
'dotx',
'dotm',
'rtf',
'odt',
'ott',
'txt',
'md',
'mhtml',
'mht',
'html',
'htm',
'xml',
'wps',
'wri',
// Spreadsheets
'xls',
'xlsx',
'xlsm',
'xlsb',
'xltx',
'xltm',
'ods',
'ots',
'csv',
// Presentations
'ppt',
'pptx',
'pptm',
'pps',
'ppsx',
'pot',
'potx',
'odp',
'otp'
].includes(props.doc.ext) ||
(props.doc.size > 500000 &&
['avif', 'webp', 'png', 'jpg', 'jpeg'].includes(props.doc.ext))
const preview = () => {
const store = useMainStore()
const ext = props.doc.ext
// Office document previews may be optionally disabled server-side
if (store.server.office_previews === false) {
const officeExts = [
'doc', 'dot', 'docx', 'docm', 'dotx', 'dotm', 'rtf',
'odt', 'ott', 'txt', 'md', 'mhtml', 'mht', 'html',
'htm', 'xml', 'wps', 'wri',
'xls', 'xlsx', 'xlsm', 'xlsb', 'xltx', 'xltm',
'ods', 'ots', 'csv',
'ppt', 'pptx', 'pptm', 'pps', 'ppsx',
'pot', 'potx', 'odp', 'otp'
]
if (officeExts.includes(ext)) return false
}
return (
[
'bmp',
'ico',
'tif',
'tiff',
'heic',
'heif',
'pdf',
'epub',
'mobi',
// Documents
'doc',
'dot',
'docx',
'docm',
'dotx',
'dotm',
'rtf',
'odt',
'ott',
'txt',
'md',
'mhtml',
'mht',
'html',
'htm',
'xml',
'wps',
'wri',
// Spreadsheets
'xls',
'xlsx',
'xlsm',
'xlsb',
'xltx',
'xltm',
'ods',
'ots',
'csv',
// Presentations
'ppt',
'pptx',
'pptm',
'pps',
'ppsx',
'pot',
'potx',
'odp',
'otp'
].includes(ext) ||
(props.doc.size > 500000 &&
['avif', 'webp', 'png', 'jpg', 'jpeg'].includes(ext))
)
}
</script>
<style scoped>
+16 -1
View File
@@ -84,6 +84,21 @@ export class Doc {
// Folders cannot be previewable
if (this.dir) return false
if (this.img) return true
const store = useMainStore()
const ext = this.ext
// Office document previews may be optionally disabled server-side
if (store.server.office_previews === false) {
const officeExts = [
'doc', 'dot', 'docx', 'docm', 'dotx', 'dotm', 'rtf',
'odt', 'ott', 'txt', 'md', 'mhtml', 'mht', 'html',
'htm', 'xml', 'wps', 'wri',
'xls', 'xlsx', 'xlsm', 'xlsb', 'xltx', 'xltm',
'ods', 'ots', 'csv',
'ppt', 'pptx', 'pptm', 'pps', 'ppsx',
'pot', 'potx', 'odp', 'otp'
]
if (officeExts.includes(ext)) return false
}
// Not a comprehensive list, but good enough for now
return [
'mp4',
@@ -133,7 +148,7 @@ export class Doc {
'potx',
'odp',
'otp'
].includes(this.ext)
].includes(ext)
}
get previewurl(): string {
if (!this.complete || !this.previewable) return ''
+1 -1
View File
@@ -79,7 +79,7 @@ export const useMainStore = defineStore('main', {
connected: false,
authInProgress: false,
cursor: '' as string,
server: {} as Record<string, any> & { public?: boolean; paskia?: boolean },
server: {} as Record<string, any> & { public?: boolean; paskia?: boolean; office_previews?: boolean },
dialog: '' as '' | 'settings' | 'usermgmt' | 'accessdenied' | 'tokens',
uprogress: {} as any,
dprogress: {} as any,