463 lines
15 KiB
Go
463 lines
15 KiB
Go
package processor
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"eslogad-be/internal/entities"
|
|
"eslogad-be/internal/repository"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type OnlyOfficeProcessor interface {
|
|
GetDocumentSessionByKey(ctx context.Context, documentKey string) (*entities.DocumentSession, error)
|
|
GetOrCreateDocumentSession(ctx context.Context, documentID, userID uuid.UUID) (*entities.DocumentSession, error)
|
|
UpdateDocumentSession(ctx context.Context, session *entities.DocumentSession) error
|
|
CreateDocumentVersion(ctx context.Context, version *entities.DocumentVersion) error
|
|
UpdateDocumentURL(ctx context.Context, documentID uuid.UUID, newURL string) error
|
|
LockDocument(ctx context.Context, documentID, userID uuid.UUID) error
|
|
UnlockDocument(ctx context.Context, documentID, userID uuid.UUID) error
|
|
LogDocumentError(ctx context.Context, documentID uuid.UUID, errorMsg string, details interface{}) error
|
|
GetDocumentDetails(ctx context.Context, documentID uuid.UUID, documentType string) (*DocumentDetails, error)
|
|
}
|
|
|
|
type DocumentDetails struct {
|
|
DocumentID uuid.UUID
|
|
FileName string
|
|
FileType string
|
|
FileURL string
|
|
FileSize int64
|
|
DocumentType string
|
|
ReferenceID uuid.UUID
|
|
}
|
|
|
|
type OnlyOfficeProcessorImpl struct {
|
|
db *gorm.DB
|
|
sessionRepo *repository.DocumentSessionRepository
|
|
versionRepo *repository.DocumentVersionRepository
|
|
metadataRepo *repository.DocumentMetadataRepository
|
|
errorRepo *repository.DocumentErrorRepository
|
|
txManager *repository.TxManager
|
|
}
|
|
|
|
func NewOnlyOfficeProcessor(
|
|
db *gorm.DB,
|
|
sessionRepo *repository.DocumentSessionRepository,
|
|
versionRepo *repository.DocumentVersionRepository,
|
|
metadataRepo *repository.DocumentMetadataRepository,
|
|
errorRepo *repository.DocumentErrorRepository,
|
|
txManager *repository.TxManager,
|
|
) *OnlyOfficeProcessorImpl {
|
|
return &OnlyOfficeProcessorImpl{
|
|
db: db,
|
|
sessionRepo: sessionRepo,
|
|
versionRepo: versionRepo,
|
|
metadataRepo: metadataRepo,
|
|
errorRepo: errorRepo,
|
|
txManager: txManager,
|
|
}
|
|
}
|
|
|
|
// GetDocumentSessionByKey retrieves a document session by its OnlyOffice key
|
|
func (p *OnlyOfficeProcessorImpl) GetDocumentSessionByKey(ctx context.Context, documentKey string) (*entities.DocumentSession, error) {
|
|
return p.sessionRepo.GetByKey(ctx, documentKey)
|
|
}
|
|
|
|
// GetOrCreateDocumentSession gets an existing session or creates a new one
|
|
func (p *OnlyOfficeProcessorImpl) GetOrCreateDocumentSession(ctx context.Context, documentID, userID uuid.UUID) (*entities.DocumentSession, error) {
|
|
// Try to get existing active session
|
|
session, err := p.sessionRepo.GetActiveByDocument(ctx, documentID)
|
|
if err == nil && session != nil {
|
|
// Return existing session if it's not closed
|
|
if session.Status != 4 { // Status 4 = closed
|
|
return session, nil
|
|
}
|
|
}
|
|
|
|
// Create new session
|
|
session = &entities.DocumentSession{
|
|
DocumentID: documentID,
|
|
UserID: userID,
|
|
Status: 0, // Initial status
|
|
Version: 1,
|
|
IsLocked: false,
|
|
}
|
|
|
|
// Generate unique document key with retry logic
|
|
for attempts := 0; attempts < 3; attempts++ {
|
|
session.DocumentKey = p.generateDocumentKey(documentID, userID)
|
|
err = p.sessionRepo.Create(ctx, session)
|
|
if err == nil {
|
|
return session, nil
|
|
}
|
|
// If it's not a duplicate key error, return the error
|
|
if !errors.Is(err, gorm.ErrDuplicatedKey) && !contains(err.Error(), "duplicate key") {
|
|
return nil, fmt.Errorf("failed to create document session: %w", err)
|
|
}
|
|
// Wait a bit before retrying
|
|
time.Sleep(time.Millisecond * 10)
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to create document session after retries: %w", err)
|
|
}
|
|
|
|
// UpdateDocumentSession updates an existing document session
|
|
func (p *OnlyOfficeProcessorImpl) UpdateDocumentSession(ctx context.Context, session *entities.DocumentSession) error {
|
|
return p.sessionRepo.Update(ctx, session)
|
|
}
|
|
|
|
// CreateDocumentVersion creates a new document version
|
|
func (p *OnlyOfficeProcessorImpl) CreateDocumentVersion(ctx context.Context, version *entities.DocumentVersion) error {
|
|
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
|
// Deactivate previous versions if this is the active one
|
|
if version.IsActive {
|
|
err := p.versionRepo.DeactivateAllVersions(txCtx, version.DocumentID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to deactivate previous versions: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create new version
|
|
err := p.versionRepo.Create(txCtx, version)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create document version: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// UpdateDocumentURL updates the document URL in the appropriate table
|
|
func (p *OnlyOfficeProcessorImpl) UpdateDocumentURL(ctx context.Context, documentID uuid.UUID, newURL string) error {
|
|
// Get document metadata to determine type
|
|
metadata, err := p.metadataRepo.GetByDocumentID(ctx, documentID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get document metadata: %w", err)
|
|
}
|
|
|
|
// Update based on document type
|
|
switch metadata.DocumentType {
|
|
case "letter_attachment":
|
|
return p.updateLetterAttachmentURL(ctx, metadata.ReferenceID, newURL)
|
|
case "outgoing_attachment":
|
|
return p.updateOutgoingAttachmentURL(ctx, metadata.ReferenceID, newURL)
|
|
case "discussion_attachment":
|
|
return p.updateDiscussionAttachmentURL(ctx, metadata.ReferenceID, newURL)
|
|
default:
|
|
return fmt.Errorf("unknown document type: %s", metadata.DocumentType)
|
|
}
|
|
}
|
|
|
|
// LockDocument locks a document for editing
|
|
func (p *OnlyOfficeProcessorImpl) LockDocument(ctx context.Context, documentID, userID uuid.UUID) error {
|
|
session, err := p.sessionRepo.GetActiveByDocument(ctx, documentID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get document session: %w", err)
|
|
}
|
|
|
|
// Check if already locked by another user
|
|
if session.IsLocked && session.LockedBy != nil && *session.LockedBy != userID {
|
|
return errors.New("document is already locked by another user")
|
|
}
|
|
|
|
// Lock the document
|
|
session.IsLocked = true
|
|
session.LockedBy = &userID
|
|
now := time.Now()
|
|
session.LockedAt = &now
|
|
|
|
return p.sessionRepo.Update(ctx, session)
|
|
}
|
|
|
|
// UnlockDocument unlocks a document
|
|
func (p *OnlyOfficeProcessorImpl) UnlockDocument(ctx context.Context, documentID, userID uuid.UUID) error {
|
|
session, err := p.sessionRepo.GetActiveByDocument(ctx, documentID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get document session: %w", err)
|
|
}
|
|
|
|
// Check if locked by the requesting user
|
|
if session.IsLocked && session.LockedBy != nil && *session.LockedBy != userID {
|
|
return errors.New("document is locked by another user")
|
|
}
|
|
|
|
// Unlock the document
|
|
session.IsLocked = false
|
|
session.LockedBy = nil
|
|
session.LockedAt = nil
|
|
|
|
return p.sessionRepo.Update(ctx, session)
|
|
}
|
|
|
|
// LogDocumentError logs an error related to document operations
|
|
func (p *OnlyOfficeProcessorImpl) LogDocumentError(ctx context.Context, documentID uuid.UUID, errorMsg string, details interface{}) error {
|
|
detailsMap := make(map[string]interface{})
|
|
|
|
// Convert details to map if possible
|
|
if details != nil {
|
|
detailsJSON, _ := json.Marshal(details)
|
|
json.Unmarshal(detailsJSON, &detailsMap)
|
|
}
|
|
|
|
docError := &entities.DocumentError{
|
|
DocumentID: documentID,
|
|
ErrorType: "onlyoffice_callback",
|
|
ErrorMsg: errorMsg,
|
|
Details: detailsMap,
|
|
}
|
|
|
|
// Get current session if available
|
|
if session, err := p.sessionRepo.GetActiveByDocument(ctx, documentID); err == nil && session != nil {
|
|
docError.SessionID = &session.ID
|
|
}
|
|
|
|
return p.errorRepo.Create(ctx, docError)
|
|
}
|
|
|
|
// GetDocumentDetails retrieves document details based on type
|
|
func (p *OnlyOfficeProcessorImpl) GetDocumentDetails(ctx context.Context, documentID uuid.UUID, documentType string) (*DocumentDetails, error) {
|
|
// Get metadata
|
|
metadata, err := p.metadataRepo.GetByDocumentID(ctx, documentID)
|
|
if err != nil {
|
|
// If metadata doesn't exist, create it based on the document type
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return p.createDocumentMetadata(ctx, documentID, documentType)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Get the actual file URL from the appropriate table
|
|
fileURL, err := p.getDocumentURL(ctx, metadata)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &DocumentDetails{
|
|
DocumentID: metadata.DocumentID,
|
|
FileName: metadata.FileName,
|
|
FileType: metadata.FileType,
|
|
FileURL: fileURL,
|
|
FileSize: metadata.FileSize,
|
|
DocumentType: metadata.DocumentType,
|
|
ReferenceID: metadata.ReferenceID,
|
|
}, nil
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
func (p *OnlyOfficeProcessorImpl) generateDocumentKey(documentID, userID uuid.UUID) string {
|
|
// Use nanoseconds for better precision
|
|
now := time.Now().UnixNano()
|
|
|
|
// Generate random bytes for additional uniqueness
|
|
randomBytes := make([]byte, 4)
|
|
rand.Read(randomBytes)
|
|
randomHex := hex.EncodeToString(randomBytes)
|
|
|
|
return fmt.Sprintf("%s_%s_%d_%s", documentID.String()[:8], userID.String()[:8], now, randomHex)
|
|
}
|
|
|
|
func (p *OnlyOfficeProcessorImpl) updateLetterAttachmentURL(ctx context.Context, attachmentID uuid.UUID, newURL string) error {
|
|
// Update letter attachment URL
|
|
return p.db.WithContext(ctx).
|
|
Table("letter_incoming_attachments").
|
|
Where("id = ?", attachmentID).
|
|
Update("file_url", newURL).Error
|
|
}
|
|
|
|
func (p *OnlyOfficeProcessorImpl) updateOutgoingAttachmentURL(ctx context.Context, attachmentID uuid.UUID, newURL string) error {
|
|
// Update outgoing letter attachment URL
|
|
return p.db.WithContext(ctx).
|
|
Table("letter_outgoing_attachments").
|
|
Where("id = ?", attachmentID).
|
|
Update("file_url", newURL).Error
|
|
}
|
|
|
|
func (p *OnlyOfficeProcessorImpl) updateDiscussionAttachmentURL(ctx context.Context, attachmentID uuid.UUID, newURL string) error {
|
|
// Update discussion attachment URL
|
|
return p.db.WithContext(ctx).
|
|
Table("letter_outgoing_discussion_attachments").
|
|
Where("id = ?", attachmentID).
|
|
Update("file_url", newURL).Error
|
|
}
|
|
|
|
func (p *OnlyOfficeProcessorImpl) createDocumentMetadata(ctx context.Context, documentID uuid.UUID, documentType string) (*DocumentDetails, error) {
|
|
var metadata entities.DocumentMetadata
|
|
var details DocumentDetails
|
|
|
|
// Fetch document information based on type
|
|
switch documentType {
|
|
case "letter_attachment":
|
|
var attachment struct {
|
|
ID uuid.UUID `gorm:"column:id"`
|
|
LetterID uuid.UUID `gorm:"column:letter_id"`
|
|
FileName string `gorm:"column:file_name"`
|
|
FileURL string `gorm:"column:file_url"`
|
|
FileSize int64 `gorm:"column:file_size"`
|
|
FileType string `gorm:"column:file_type"`
|
|
}
|
|
err := p.db.WithContext(ctx).
|
|
Table("letter_incoming_attachments").
|
|
Where("id = ?", documentID).
|
|
First(&attachment).Error
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get letter attachment: %w", err)
|
|
}
|
|
|
|
metadata = entities.DocumentMetadata{
|
|
DocumentID: documentID,
|
|
DocumentType: documentType,
|
|
ReferenceID: attachment.LetterID,
|
|
FileName: attachment.FileName,
|
|
FileType: attachment.FileType,
|
|
FileSize: attachment.FileSize,
|
|
}
|
|
details = DocumentDetails{
|
|
DocumentID: documentID,
|
|
FileName: attachment.FileName,
|
|
FileType: attachment.FileType,
|
|
FileURL: attachment.FileURL,
|
|
FileSize: attachment.FileSize,
|
|
DocumentType: documentType,
|
|
ReferenceID: attachment.LetterID,
|
|
}
|
|
|
|
case "outgoing_attachment":
|
|
var attachment struct {
|
|
ID uuid.UUID `gorm:"column:id"`
|
|
LetterID uuid.UUID `gorm:"column:letter_id"`
|
|
FileName string `gorm:"column:file_name"`
|
|
FileURL string `gorm:"column:file_url"`
|
|
FileSize int64 `gorm:"column:file_size"`
|
|
FileType string `gorm:"column:file_type"`
|
|
}
|
|
err := p.db.WithContext(ctx).
|
|
Table("letter_outgoing_attachments").
|
|
Where("id = ?", documentID).
|
|
First(&attachment).Error
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get outgoing letter attachment: %w", err)
|
|
}
|
|
|
|
metadata = entities.DocumentMetadata{
|
|
DocumentID: documentID,
|
|
DocumentType: documentType,
|
|
ReferenceID: attachment.LetterID,
|
|
FileName: attachment.FileName,
|
|
FileType: attachment.FileType,
|
|
FileSize: attachment.FileSize,
|
|
}
|
|
details = DocumentDetails{
|
|
DocumentID: documentID,
|
|
FileName: attachment.FileName,
|
|
FileType: attachment.FileType,
|
|
FileURL: attachment.FileURL,
|
|
FileSize: attachment.FileSize,
|
|
DocumentType: documentType,
|
|
ReferenceID: attachment.LetterID,
|
|
}
|
|
|
|
case "discussion_attachment":
|
|
var attachment struct {
|
|
ID uuid.UUID `gorm:"column:id"`
|
|
DiscussionID uuid.UUID `gorm:"column:discussion_id"`
|
|
FileName string `gorm:"column:file_name"`
|
|
FileURL string `gorm:"column:file_url"`
|
|
FileSize int64 `gorm:"column:file_size"`
|
|
FileType string `gorm:"column:file_type"`
|
|
}
|
|
err := p.db.WithContext(ctx).
|
|
Table("letter_outgoing_discussion_attachments").
|
|
Where("id = ?", documentID).
|
|
First(&attachment).Error
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get discussion attachment: %w", err)
|
|
}
|
|
|
|
metadata = entities.DocumentMetadata{
|
|
DocumentID: documentID,
|
|
DocumentType: documentType,
|
|
ReferenceID: attachment.DiscussionID,
|
|
FileName: attachment.FileName,
|
|
FileType: attachment.FileType,
|
|
FileSize: attachment.FileSize,
|
|
}
|
|
details = DocumentDetails{
|
|
DocumentID: documentID,
|
|
FileName: attachment.FileName,
|
|
FileType: attachment.FileType,
|
|
FileURL: attachment.FileURL,
|
|
FileSize: attachment.FileSize,
|
|
DocumentType: documentType,
|
|
ReferenceID: attachment.DiscussionID,
|
|
}
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown document type: %s", documentType)
|
|
}
|
|
|
|
// Create metadata in database
|
|
err := p.metadataRepo.Create(ctx, &metadata)
|
|
if err != nil {
|
|
// If it already exists (race condition), try to get it
|
|
if strings.Contains(err.Error(), "duplicate key") {
|
|
existingMetadata, getErr := p.metadataRepo.GetByDocumentID(ctx, documentID)
|
|
if getErr == nil {
|
|
// Get the file URL
|
|
fileURL, urlErr := p.getDocumentURL(ctx, existingMetadata)
|
|
if urlErr == nil {
|
|
details.FileURL = fileURL
|
|
}
|
|
return &details, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("failed to create document metadata: %w", err)
|
|
}
|
|
|
|
return &details, nil
|
|
}
|
|
|
|
func (p *OnlyOfficeProcessorImpl) getDocumentURL(ctx context.Context, metadata *entities.DocumentMetadata) (string, error) {
|
|
var fileURL string
|
|
|
|
switch metadata.DocumentType {
|
|
case "letter_attachment":
|
|
err := p.db.WithContext(ctx).
|
|
Table("letter_incoming_attachments").
|
|
Where("id = ?", metadata.ReferenceID).
|
|
Select("file_url").
|
|
Scan(&fileURL).Error
|
|
return fileURL, err
|
|
|
|
case "outgoing_attachment":
|
|
err := p.db.WithContext(ctx).
|
|
Table("letter_outgoing_attachments").
|
|
Where("id = ?", metadata.ReferenceID).
|
|
Select("file_url").
|
|
Scan(&fileURL).Error
|
|
return fileURL, err
|
|
|
|
case "discussion_attachment":
|
|
err := p.db.WithContext(ctx).
|
|
Table("letter_outgoing_discussion_attachments").
|
|
Where("id = ?", metadata.ReferenceID).
|
|
Select("file_url").
|
|
Scan(&fileURL).Error
|
|
return fileURL, err
|
|
|
|
default:
|
|
return "", fmt.Errorf("unknown document type: %s", metadata.DocumentType)
|
|
}
|
|
}
|
|
|
|
// Helper function to check if string contains substring
|
|
func contains(s, substr string) bool {
|
|
return strings.Contains(s, substr)
|
|
} |