Feat: Live thumbnails for office files
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m26s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m26s
This commit is contained in:
parent
b80abdbefb
commit
80d60d540a
10 changed files with 244 additions and 106 deletions
|
|
@ -13,6 +13,13 @@ import (
|
|||
"git.elijahkuntz.com/Elijah/drive/config"
|
||||
"git.elijahkuntz.com/Elijah/drive/database"
|
||||
"github.com/disintegration/imaging"
|
||||
"git.elijahkuntz.com/Elijah/drive/middleware"
|
||||
"net/url"
|
||||
"io"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type ThumbnailJob struct {
|
||||
|
|
@ -70,14 +77,6 @@ func (m *ThumbnailManager) Enqueue(job ThumbnailJob) {
|
|||
func (m *ThumbnailManager) worker(id int) {
|
||||
defer m.wg.Done()
|
||||
for job := range m.Queue {
|
||||
// Check if thumbnail generation is enabled
|
||||
if val, err := m.DB.GetSetting("thumbnail_images"); err == nil && val == "false" && strings.HasPrefix(job.MimeType, "image/") {
|
||||
continue
|
||||
}
|
||||
if val, err := m.DB.GetSetting("thumbnail_videos"); err == nil && val == "false" && strings.HasPrefix(job.MimeType, "video/") {
|
||||
continue
|
||||
}
|
||||
|
||||
destPath := filepath.Join(m.Config.ThumbnailDir, job.Checksum+".jpg")
|
||||
|
||||
// Skip if thumbnail already exists
|
||||
|
|
@ -92,6 +91,10 @@ func (m *ThumbnailManager) worker(id int) {
|
|||
err = generateImageThumbnail(fullSourcePath, destPath)
|
||||
} else if strings.HasPrefix(job.MimeType, "video/") {
|
||||
err = generateVideoThumbnail(fullSourcePath, destPath)
|
||||
} else if strings.HasSuffix(job.FilePath, ".docx") || strings.HasSuffix(job.FilePath, ".pptx") || strings.HasSuffix(job.FilePath, ".xlsx") {
|
||||
err = generateOfficeThumbnail(job.FilePath, destPath, m.Config)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -131,3 +134,109 @@ func generateVideoThumbnail(src, dest string) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateOfficeThumbnail(filePath, destPath string, cfg *config.Config) error {
|
||||
// Generate a download token for the Conversion API to fetch the file from our backend
|
||||
token, err := middleware.GenerateDownloadToken(0, "system_worker", cfg.JWTSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate download token: %w", err)
|
||||
}
|
||||
|
||||
// Assuming backend runs on port from config, but we need the external URL or internal docker URL that Document Server can reach.
|
||||
// Since we saw in OnlyOfficeEditor that the Document Server can reach the host at 192.168.50.81:5827, we use that.
|
||||
// We'll try to build a robust URL.
|
||||
hostIP := "192.168.50.81" // Hardcoded as per OnlyOfficeEditor.tsx implementation for this specific deployment
|
||||
port := cfg.Port
|
||||
fileUrl := fmt.Sprintf("http://%s:%s/api/public/onlyoffice/download?path=%s&token=%s", hostIP, port, url.QueryEscape(filePath), token)
|
||||
|
||||
ext := strings.TrimPrefix(filepath.Ext(filePath), ".")
|
||||
if ext == "" {
|
||||
ext = "docx"
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"async": false,
|
||||
"filetype": ext,
|
||||
"key": fmt.Sprintf("thumb_%d", time.Now().UnixNano()),
|
||||
"outputtype": "jpg",
|
||||
"title": "preview.jpg",
|
||||
"url": fileUrl,
|
||||
}
|
||||
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://office.elijahkuntz.com/ConvertService.ashx", bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
// Sign the payload with the OnlyOffice JWT
|
||||
if cfg.OnlyOfficeJWT != "" {
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(payload))
|
||||
signedToken, err := jwtToken.SignedString([]byte(cfg.OnlyOfficeJWT))
|
||||
if err == nil {
|
||||
req.Header.Set("Authorization", "Bearer "+signedToken)
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("conversion request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("conversion request returned status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Error *int `json:"error,omitempty"`
|
||||
FileUrl string `json:"fileUrl,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return fmt.Errorf("failed to decode conversion response: %w", err)
|
||||
}
|
||||
|
||||
// Wait, the API might return error 0 for success
|
||||
if result.Error != nil && *result.Error != 0 {
|
||||
return fmt.Errorf("conversion service returned error code: %d", *result.Error)
|
||||
}
|
||||
|
||||
if result.FileUrl == "" {
|
||||
return fmt.Errorf("no fileUrl in conversion response")
|
||||
}
|
||||
|
||||
// Download the resulting image
|
||||
imgResp, err := http.Get(result.FileUrl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download converted image: %w", err)
|
||||
}
|
||||
defer imgResp.Body.Close()
|
||||
|
||||
if imgResp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("downloading image returned status: %d", imgResp.StatusCode)
|
||||
}
|
||||
|
||||
// Create temp file for the downloaded image
|
||||
tmpFile, err := os.CreateTemp("", "office_thumb_*.jpg")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpName := tmpFile.Name()
|
||||
defer os.Remove(tmpName)
|
||||
|
||||
if _, err := io.Copy(tmpFile, imgResp.Body); err != nil {
|
||||
tmpFile.Close()
|
||||
return err
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
// Resize it using imaging to ensure it matches standard thumbnail sizes
|
||||
return generateImageThumbnail(tmpName, destPath)
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue