ultisuite-backend/internal/api/richtext/public_share.go
R3D347HR4Y d4ccf7eb6e
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
hocuspocus lol 2
2026-06-09 14:30:34 +02:00

210 lines
6.4 KiB
Go

package richtext
import (
"context"
"encoding/json"
"fmt"
"io"
"net/url"
"strings"
"time"
)
type PublicSessionResult struct {
SessionResult
DocumentURL string `json:"documentUrl,omitempty"`
SaveURL string `json:"saveUrl,omitempty"`
}
func (s *Service) CreatePublicSession(ctx context.Context, token, filePath, mode, password, guestID, guestName, displayName string) (*PublicSessionResult, error) {
if !s.Cfg.Enabled {
return nil, fmt.Errorf("rich text editor disabled")
}
filePath = normalizePath(filePath)
canonical, source, importRequired, err := s.resolvePublicCanonicalPath(ctx, token, filePath, password, displayName)
if err != nil {
return nil, err
}
ownerID, ownerPath, err := s.ownerSidecarPathForPublic(ctx, token, password, canonical, displayName)
if err != nil {
return nil, err
}
roomID, err := s.resolveCollabRoomID(ctx, ownerID, ownerPath)
if err != nil {
return nil, err
}
tokenJWT, err := signRoomToken(roomTokenPayload{
Room: roomID,
Path: canonical,
User: "public:" + token,
Sub: guestID,
Name: guestName,
Mode: mode,
Expires: time.Now().Add(8 * time.Hour).Unix(),
}, s.Cfg.HocuspocusSecret)
if err != nil {
return nil, err
}
apiBase := strings.TrimRight(s.Cfg.APIInternalURL, "/")
sig, _ := signPublicDocAccess(token, canonical, password, s.Cfg.HocuspocusSecret)
docURL := fmt.Sprintf("%s/api/v1/drive/public/shares/%s/richtext/document?path=%s&password=%s&sig=%s",
apiBase, url.PathEscape(token), url.QueryEscape(canonical), url.QueryEscape(password), url.QueryEscape(sig))
saveURL := docURL
wsURL := strings.TrimSpace(s.Cfg.HocuspocusPublicURL)
collab := wsURL != "" && s.Cfg.HocuspocusSecret != ""
return &PublicSessionResult{
SessionResult: SessionResult{
RoomID: roomID,
CanonicalPath: canonical,
SourcePath: source,
WsURL: wsURL,
Token: tokenJWT,
Mode: mode,
ImportRequired: importRequired,
Collaboration: collab,
},
DocumentURL: docURL,
SaveURL: saveURL,
}, nil
}
func (s *Service) resolvePublicCanonicalPath(ctx context.Context, token, filePath, password, displayName string) (canonical, source string, importRequired bool, err error) {
filePath = normalizePath(filePath)
source = filePath
if source == "/" {
source = s.publicClientSourcePath(ctx, token, password, filePath, displayName)
}
if isUltidocPath(filePath) {
return filePath, "", false, nil
}
if isUltidocPath(source) {
return source, "", false, nil
}
sidecar := sidecarPathForSource(source)
if s.publicSidecarExists(ctx, token, password, sidecar, displayName) {
return sidecar, source, false, nil
}
if _, err := s.publicFileExists(ctx, token, source, password); err != nil {
return "", "", false, fmt.Errorf("file not found")
}
return sidecar, source, true, nil
}
func (s *Service) publicClientSourcePath(ctx context.Context, token, password, clientPath, displayName string) string {
binding, err := s.nc.ResolvePublicShareBinding(ctx, token, password)
if err == nil {
return binding.ClientSourcePath(clientPath, displayName)
}
if name := strings.TrimSpace(displayName); name != "" {
return normalizePath("/" + name)
}
return clientPath
}
func (s *Service) publicSidecarExists(ctx context.Context, token, password, clientSidecar, displayName string) bool {
if _, err := s.publicFileExists(ctx, token, clientSidecar, password); err == nil {
return true
}
binding, err := s.nc.ResolvePublicShareBinding(ctx, token, password)
if err != nil {
return false
}
ownerPath := binding.OwnerPathForClient(clientSidecar, displayName)
if _, err := s.nc.FileRevision(ctx, binding.OwnerID, ownerPath); err == nil {
return true
}
return false
}
func (s *Service) ImportPublicDocument(ctx context.Context, token, password, displayName string, req ImportRequest) (string, error) {
source := normalizePath(req.SourcePath)
canonical, _, importRequired, err := s.resolvePublicCanonicalPath(ctx, token, source, password, displayName)
if err != nil {
return "", err
}
if !importRequired {
return canonical, nil
}
var content json.RawMessage
if len(req.Content) > 0 {
content = req.Content
} else {
body, err := s.LoadPublicDocument(ctx, token, source, password, displayName)
if err != nil {
return "", err
}
name := fileNameFromPath(source)
content, err = ImportBytes(name, "", body)
if err != nil {
return "", err
}
}
doc := NewUltiDoc(content, &UltiDocSource{
Path: source,
ImportedAt: time.Now().UTC().Format(time.RFC3339),
})
payload, err := doc.Marshal()
if err != nil {
return "", err
}
if err := s.SavePublicDocument(ctx, token, canonical, password, displayName, payload); err != nil {
return "", err
}
return canonical, nil
}
func (s *Service) publicFileExists(ctx context.Context, token, path, password string) (bool, error) {
_, err := s.nc.PublicShareFileRevision(ctx, token, path, password)
if err != nil {
return false, err
}
return true, nil
}
func (s *Service) LoadPublicDocument(ctx context.Context, token, clientPath, password, displayName string) ([]byte, error) {
if binding, err := s.nc.ResolvePublicShareBinding(ctx, token, password); err == nil {
ownerPath := binding.OwnerPathForClient(clientPath, displayName)
body, _, err := s.nc.Download(ctx, binding.OwnerID, ownerPath)
if err == nil {
defer body.Close()
return io.ReadAll(body)
}
}
body, _, err := s.nc.DownloadPublicShare(ctx, token, clientPath, password)
if err != nil {
return nil, err
}
defer body.Close()
return io.ReadAll(body)
}
func (s *Service) SavePublicDocument(ctx context.Context, token, clientPath, password, displayName string, raw []byte) error {
if binding, err := s.nc.ResolvePublicShareBinding(ctx, token, password); err == nil {
ownerPath := binding.OwnerPathForClient(clientPath, displayName)
reader := strings.NewReader(string(raw))
if err := s.nc.Upload(ctx, binding.OwnerID, ownerPath, reader, "application/json"); err == nil {
return nil
}
}
reader := strings.NewReader(string(raw))
return s.nc.UploadPublicShare(ctx, token, clientPath, password, reader, "application/json")
}
func (s *Service) LoadPublicDocumentLegacy(ctx context.Context, token, path, password string) ([]byte, error) {
return s.LoadPublicDocument(ctx, token, path, password, "")
}
func (s *Service) SavePublicDocumentLegacy(ctx context.Context, token, path, password string, raw []byte) error {
return s.SavePublicDocument(ctx, token, path, password, "", raw)
}