Implement public WOPI routes for shared files and integrate Collabora for document viewing
This commit is contained in:
BIN
go_cloud/api
BIN
go_cloud/api
Binary file not shown.
@@ -380,6 +380,15 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Range")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
// Public WOPI routes for shared files
|
||||
r.Route("/wopi/share/{token}", func(r chi.Router) {
|
||||
r.Get("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
publicWopiCheckFileInfoHandler(w, req, db, jwtManager)
|
||||
})
|
||||
r.Get("/contents", func(w http.ResponseWriter, req *http.Request) {
|
||||
publicWopiGetFileHandler(w, req, db, cfg, jwtManager)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return r
|
||||
@@ -2970,9 +2979,14 @@ func publicFileShareHandler(w http.ResponseWriter, r *http.Request, db *database
|
||||
Token: viewerToken,
|
||||
}
|
||||
|
||||
// Set view URL for PDFs, videos, and audio (for inline viewing)
|
||||
// Set view URL for PDFs, videos, audio, and documents (for inline viewing)
|
||||
if isPdf || strings.HasPrefix(mimeType, "video/") || strings.HasPrefix(mimeType, "audio/") {
|
||||
viewerSession.ViewUrl = viewPath
|
||||
} else if strings.Contains(mimeType, "document") || strings.Contains(mimeType, "word") || strings.Contains(mimeType, "spreadsheet") || strings.Contains(mimeType, "presentation") {
|
||||
// Use Collabora for document viewing
|
||||
wopiSrc := fmt.Sprintf("%s://%s/public/wopi/share/%s", scheme, host, token)
|
||||
collaboraUrl := fmt.Sprintf("https://of.b0esche.cloud/lool/dist/mobile/cool.html?WOPISrc=%s", url.QueryEscape(wopiSrc))
|
||||
viewerSession.ViewUrl = collaboraUrl
|
||||
}
|
||||
|
||||
viewerSession.Capabilities.CanEdit = false
|
||||
@@ -3359,3 +3373,172 @@ func revokeUserFileShareLinkHandler(w http.ResponseWriter, r *http.Request, db *
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// publicWopiCheckFileInfoHandler handles GET /public/wopi/share/{token}
|
||||
// Returns metadata about the shared file for Collabora viewer
|
||||
func publicWopiCheckFileInfoHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager) {
|
||||
token := chi.URLParam(r, "token")
|
||||
if token == "" {
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Token required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get share link
|
||||
link, err := db.GetFileShareLinkByToken(r.Context(), token)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
errors.WriteError(w, errors.CodeNotFound, "Link not found or expired", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
errors.LogError(r, err, "Failed to get share link")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get file metadata
|
||||
file, err := db.GetFileByID(r.Context(), link.FileID)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to get file")
|
||||
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if file.UserID == nil {
|
||||
errors.WriteError(w, errors.CodeNotFound, "File not accessible", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
lastModifiedTime := file.LastModified
|
||||
if lastModifiedTime.IsZero() {
|
||||
lastModifiedTime = time.Now()
|
||||
}
|
||||
|
||||
response := struct {
|
||||
BaseFileName string `json:"BaseFileName"`
|
||||
Size int64 `json:"Size"`
|
||||
OwnerId string `json:"OwnerId"`
|
||||
Version string `json:"Version"`
|
||||
SupportsExtendedLockLength bool `json:"SupportsExtendedLockLength"`
|
||||
SupportsGetLock bool `json:"SupportsGetLock"`
|
||||
SupportsLocks bool `json:"SupportsLocks"`
|
||||
SupportsUpdate bool `json:"SupportsUpdate"`
|
||||
UserId string `json:"UserId"`
|
||||
UserFriendlyName string `json:"UserFriendlyName"`
|
||||
UserCanWrite bool `json:"UserCanWrite"`
|
||||
UserCanNotWriteRelative bool `json:"UserCanNotWriteRelative"`
|
||||
ReadOnly bool `json:"ReadOnly"`
|
||||
RestrictedWebViewOnly bool `json:"RestrictedWebViewOnly"`
|
||||
UserCanCreateRelativeToFolder bool `json:"UserCanCreateRelativeToFolder"`
|
||||
EnableOwnerTermination bool `json:"EnableOwnerTermination"`
|
||||
SupportsCobalt bool `json:"SupportsCobalt"`
|
||||
SupportsDelete bool `json:"SupportsDelete"`
|
||||
SupportsRename bool `json:"SupportsRename"`
|
||||
SupportsRenameRelativeToFolder bool `json:"SupportsRenameRelativeToFolder"`
|
||||
SupportsFolders bool `json:"SupportsFolders"`
|
||||
SupportsScenarios []string `json:"SupportsScenarios"`
|
||||
LastModifiedTime string `json:"LastModifiedTime"`
|
||||
IsAnonymousUser bool `json:"IsAnonymousUser"`
|
||||
TimeZone string `json:"TimeZone"`
|
||||
}{
|
||||
BaseFileName: file.Name,
|
||||
Size: file.Size,
|
||||
OwnerId: file.UserID.String(),
|
||||
Version: file.LastModified.UTC().Format(time.RFC3339),
|
||||
SupportsExtendedLockLength: false,
|
||||
SupportsGetLock: false,
|
||||
SupportsLocks: false,
|
||||
SupportsUpdate: false,
|
||||
UserId: "anonymous",
|
||||
UserFriendlyName: "Anonymous User",
|
||||
UserCanWrite: false,
|
||||
UserCanNotWriteRelative: true,
|
||||
ReadOnly: true, // Public sharing is read-only
|
||||
RestrictedWebViewOnly: true, // Only allow web view
|
||||
UserCanCreateRelativeToFolder: false,
|
||||
EnableOwnerTermination: false,
|
||||
SupportsCobalt: false,
|
||||
SupportsDelete: false,
|
||||
SupportsRename: false,
|
||||
SupportsRenameRelativeToFolder: false,
|
||||
SupportsFolders: false,
|
||||
SupportsScenarios: []string{"embedview", "view"},
|
||||
LastModifiedTime: lastModifiedTime.UTC().Format(time.RFC3339),
|
||||
IsAnonymousUser: true,
|
||||
TimeZone: "UTC",
|
||||
}
|
||||
|
||||
fmt.Printf("[PUBLIC-WOPI] CheckFileInfo: file=%s token=%s size=%d\n", file.ID.String(), token, file.Size)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// publicWopiGetFileHandler handles GET /public/wopi/share/{token}/contents
|
||||
// Downloads the shared file content for Collabora
|
||||
func publicWopiGetFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config, jwtManager *jwt.Manager) {
|
||||
token := chi.URLParam(r, "token")
|
||||
if token == "" {
|
||||
errors.WriteError(w, errors.CodeInvalidArgument, "Token required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[PUBLIC-WOPI-GetFile] START: token=%s\n", token)
|
||||
|
||||
// Get share link
|
||||
link, err := db.GetFileShareLinkByToken(r.Context(), token)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
errors.WriteError(w, errors.CodeNotFound, "Link not found or expired", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
errors.LogError(r, err, "Failed to get share link")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get file metadata
|
||||
file, err := db.GetFileByID(r.Context(), link.FileID)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to get file")
|
||||
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if file.UserID == nil {
|
||||
errors.WriteError(w, errors.CodeNotFound, "File not accessible", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Get WebDAV client for the file's owner
|
||||
client, err := getUserWebDAVClient(r.Context(), db, *file.UserID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to get WebDAV client")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create context with longer timeout for file downloads
|
||||
downloadCtx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Stream file
|
||||
resp, err := client.Download(downloadCtx, file.Path, r.Header.Get("Range"))
|
||||
if err != nil {
|
||||
errors.LogError(r, err, "Failed to download file")
|
||||
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Copy headers from Nextcloud response
|
||||
for k, v := range resp.Header {
|
||||
w.Header()[k] = v
|
||||
}
|
||||
|
||||
// Set status code (200 or 206 for partial)
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
// Copy body
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user