feat(wopi): add WOPI protocol support for collaborative editing

Implement the Web Application Open Platform Interface (WOPI) protocol
to enable collaborative document editing with Collabora Online and
OnlyOffice through OxiCloud.

Backend:
- WOPI token service with HMAC-SHA256 signed access tokens
- WOPI lock service with in-memory lock management and expiry
- WOPI discovery service for auto-detecting editor capabilities
- WOPI HTTP handler: CheckFileInfo, GetFile, PutFile, Lock/Unlock
- File entity extended with owner_id for WOPI file-info responses
- Configuration via WOPI_* environment variables
- Services wired through DI in AppState

Frontend:
- WOPI editor component with modal and new-tab viewing modes
- Context menu integration for opening files in online editors
- Inline viewer integration for document preview

Infrastructure:
- Docker Compose file for local Collabora/OnlyOffice dev setup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jan Wiebe
2026-02-21 13:39:27 +01:00
parent 807370e194
commit 4e2c9d2592
20 changed files with 1877 additions and 14 deletions
+16
View File
@@ -41,6 +41,9 @@ pub struct File {
/// Last modification timestamp (seconds since UNIX epoch)
modified_at: u64,
/// Owner user ID (from storage.files.user_id)
owner_id: Option<String>,
}
// We no longer need this module, now we use a String directly
@@ -57,6 +60,7 @@ impl Default for File {
folder_id: None,
created_at: 0,
modified_at: 0,
owner_id: None,
}
}
}
@@ -94,6 +98,7 @@ impl File {
folder_id,
created_at: now,
modified_at: now,
owner_id: None,
})
}
@@ -124,6 +129,7 @@ impl File {
folder_id: parent_id,
created_at,
modified_at,
owner_id: None,
})
}
@@ -137,6 +143,7 @@ impl File {
folder_id: Option<String>,
created_at: u64,
modified_at: u64,
owner_id: Option<String>,
) -> FileResult<Self> {
// Validate file name
if name.is_empty() || name.contains('/') || name.contains('\\') {
@@ -156,6 +163,7 @@ impl File {
folder_id,
created_at,
modified_at,
owner_id,
})
}
@@ -196,6 +204,10 @@ impl File {
self.modified_at
}
pub fn owner_id(&self) -> Option<&str> {
self.owner_id.as_deref()
}
#[allow(clippy::too_many_arguments)]
pub fn from_dto(
id: String,
@@ -221,6 +233,7 @@ impl File {
folder_id,
created_at,
modified_at,
owner_id: None,
}
}
@@ -258,6 +271,7 @@ impl File {
folder_id: self.folder_id.clone(),
created_at: self.created_at,
modified_at: now,
owner_id: self.owner_id.clone(),
})
}
@@ -291,6 +305,7 @@ impl File {
folder_id,
created_at: self.created_at,
modified_at: now,
owner_id: self.owner_id.clone(),
})
}
@@ -311,6 +326,7 @@ impl File {
folder_id: self.folder_id.clone(),
created_at: self.created_at,
modified_at: now,
owner_id: self.owner_id.clone(),
}
}
}