Skip to content

Commit

Permalink
fix: Import notes' images from revoked sharings
Browse files Browse the repository at this point in the history
  The `io.cozy.notes.images` documents associated with a Cozy Note are
  not shared with the note itself.
  Therefore, when the sharing is revoked and the note opened by the
  sharing recipient on their own Cozy (i.e. shared notes are opened on
  their creator's Cozy while the sharing is active), included images are
  shown as missing because we can't find their associated
  `io.cozy.notes.images` documents.

  However, the image binaries themselves are stored wihtin the note's
  binary (which is an archive in this case) and thus can be imported on
  the recipient's Cozy as `io.cozy.notes.images` documents and the note
  content can be updated to reference these documents.
  After that, the images will be displayed as expected when opening the
  note.
  • Loading branch information
taratatach committed Feb 27, 2024
1 parent efbfe9c commit 8c60876
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 9 deletions.
75 changes: 66 additions & 9 deletions model/note/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"archive/tar"
"bytes"
"errors"
"fmt"
"io"
"path"
"strconv"
Expand Down Expand Up @@ -43,7 +44,7 @@ func ImportFile(inst *instance.Instance, newdoc, olddoc *vfs.FileDoc, body io.Re
}

reader := io.TeeReader(body, file)
content, err := importReader(inst, newdoc, reader, schema)
content, _, err := importReader(inst, newdoc, reader, schema)

if content != nil {
fillMetadata(newdoc, olddoc, schemaSpecs, content)
Expand All @@ -67,12 +68,66 @@ func ImportFile(inst *instance.Instance, newdoc, olddoc *vfs.FileDoc, body io.Re
return nil
}

func importReader(inst *instance.Instance, doc *vfs.FileDoc, reader io.Reader, schema *model.Schema) (*model.Node, error) {
func ImportImages(inst *instance.Instance, olddoc *vfs.FileDoc) error {
inst.Logger().WithNamespace("notes").
Infof("importing images from note: %s", olddoc.ID())
schemaSpecs := DefaultSchemaSpecs()
specs := model.SchemaSpecFromJSON(schemaSpecs)
schema, err := model.NewSchema(&specs)
if err != nil {
return fmt.Errorf("failed to read note schema: %w", err)
}

fs := inst.VFS()
file, err := fs.OpenFile(olddoc)
if err != nil {
return fmt.Errorf("failed to open file for note images import: %w", err)
}

content, images, err := importReader(inst, olddoc, file, schema)
cleanImages(inst, images) // XXX: remove images found in the archive but not in the markdown
if cerr := file.Close(); cerr != nil {
return fmt.Errorf("error while closing note file: %w", cerr)
}
if content == nil || !hasImages(images) {
inst.Logger().WithNamespace("notes").
Infof("No images to import")
return nil
}

md := markdownSerializer(images).Serialize(content)
body := []byte(md)
body, err = buildArchive(inst, []byte(md), images)
if err != nil {
return fmt.Errorf("failed to build note archive: %w", err)
}
newdoc := olddoc.Clone().(*vfs.FileDoc)
newdoc.ByteSize = int64(len(body))
newdoc.MD5Sum = nil
newdoc.Metadata["content"] = content.ToJSON()
fillMetadata(newdoc, olddoc, schemaSpecs, content)

file, err = inst.VFS().CreateFile(newdoc, olddoc)
if err != nil {
return fmt.Errorf("failed to create file for note images import: %w", err)
}
_, err = file.Write(body)
if err != nil {
err = fmt.Errorf("failed to write updated note: %w", err)
}
if cerr := file.Close(); cerr != nil && err == nil {
err = fmt.Errorf("failed to close updated note file: %w", cerr)
}

return err
}

func importReader(inst *instance.Instance, doc *vfs.FileDoc, reader io.Reader, schema *model.Schema) (*model.Node, []*Image, error) {
buf := &bytes.Buffer{}
var hasImages bool
if _, err := io.CopyN(buf, reader, 512); err != nil {
if !errors.Is(err, io.EOF) {
return nil, err
return nil, nil, fmt.Errorf("failed to buffer note content: %w", err)
}
hasImages = false
} else {
Expand All @@ -81,9 +136,10 @@ func importReader(inst *instance.Instance, doc *vfs.FileDoc, reader io.Reader, s

if !hasImages {
if _, err := buf.ReadFrom(reader); err != nil {
return nil, err
return nil, nil, err
}
return parseFile(buf, schema)
content, err := parseFile(buf, schema)
return content, nil, err
}

var content *model.Node
Expand All @@ -99,26 +155,26 @@ func importReader(inst *instance.Instance, doc *vfs.FileDoc, reader io.Reader, s
for {
header, errh := tr.Next()
if errh != nil {
return content, err
return content, images, errh
}
if header.Typeflag != tar.TypeReg {
continue
}
if header.Name == "index.md" {
content, err = parseFile(tr, schema)
if err != nil {
return nil, err
return nil, nil, fmt.Errorf("failed to parse note markdown: %w", err)
}
} else {
ext := path.Ext(header.Name)
contentType := filetype.ByExtension(ext)
upload, erru := NewImageUpload(inst, doc, header.Name, contentType)
if erru != nil {
err = erru
err = fmt.Errorf("failed to create image upload for %s: %w", header.Name, erru)
} else {
_, errc := io.Copy(upload, tr)
if cerr := upload.Close(); cerr != nil && (errc == nil || errc == io.ErrUnexpectedEOF) {
errc = cerr
errc = fmt.Errorf("failed to upload image %s: %w", header.Name, cerr)
}
if errc != nil {
err = errc
Expand All @@ -136,6 +192,7 @@ func fixURLForProsemirrorImages(node *model.Node, images []*Image) {
for _, img := range images {
if img.originalName == name {
node.Attrs["url"] = img.DocID
img.seen = true
}
}
}
Expand Down
60 changes: 60 additions & 0 deletions model/sharing/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/cozy/cozy-stack/client/request"
"github.com/cozy/cozy-stack/model/instance"
"github.com/cozy/cozy-stack/model/note"
"github.com/cozy/cozy-stack/model/vfs"
"github.com/cozy/cozy-stack/pkg/consts"
"github.com/cozy/cozy-stack/pkg/couchdb"
Expand Down Expand Up @@ -648,6 +649,65 @@ func (s *Sharing) ApplyBulkFiles(inst *instance.Instance, docs DocsList) error {
return errm
}

func (s *Sharing) GetNotes(inst *instance.Instance) ([]*vfs.FileDoc, error) {
rule := s.FirstFilesRule()
if rule != nil {
if rule.Mime != "" {
if rule.Mime == consts.NoteMimeType {
var notes []*vfs.FileDoc
req := &couchdb.AllDocsRequest{Keys: rule.Values}
if err := couchdb.GetAllDocs(inst, consts.Files, req, &notes); err != nil {
return nil, fmt.Errorf("failed to fetch notes shared by themselves: %w", err)
}

return notes, nil
} else {
return nil, nil
}
}

sharingDir, err := s.GetSharingDir(inst)
if err != nil {
return nil, fmt.Errorf("failed to get notes sharing dir: %w", err)
}

var notes []*vfs.FileDoc
fs := inst.VFS()
iter := fs.DirIterator(sharingDir, nil)
for {
_, f, err := iter.Next()
if errors.Is(err, vfs.ErrIteratorDone) {
break
}
if err != nil {
return nil, fmt.Errorf("failed to get next shared note: %w", err)
}
if f != nil && f.Mime == consts.NoteMimeType {
notes = append(notes, f)
}
}

return notes, nil
}

return nil, nil
}

func (s *Sharing) FixRevokedNotes(inst *instance.Instance) error {
docs, err := s.GetNotes(inst)
if err != nil {
return fmt.Errorf("failed to get revoked sharing notes: %w", err)
}

var errm error
for _, doc := range docs {
if err := note.ImportImages(inst, doc); err != nil {
errm = multierror.Append(errm, fmt.Errorf("failed to import revoked note images: %w", err))
}
}
return errm
}

func removeReferencesFromRule(file *vfs.FileDoc, rule *Rule) {
if rule.Selector != couchdb.SelectorReferencedBy {
return
Expand Down
9 changes: 9 additions & 0 deletions model/sharing/sharing.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,11 @@ func (s *Sharing) RevokeRecipientBySelf(inst *instance.Instance, sharingDirTrash
Warnf("RevokeRecipientBySelf failed to remove shared refs (%s)': %s", s.ID(), err)
}
if !sharingDirTrashed {
if err := s.FixRevokedNotes(inst); err != nil {
inst.Logger().WithNamespace("sharing").
Warnf("RevokeRecipientBySelf failed to fix notes for revoked sharing %s: %s", s.ID(), err)
}

if rule := s.FirstFilesRule(); rule != nil && rule.Mime == "" {
if err := s.RemoveSharingDir(inst); err != nil {
inst.Logger().WithNamespace("sharing").
Expand Down Expand Up @@ -609,6 +614,10 @@ func (s *Sharing) RevokeByNotification(inst *instance.Instance) error {
if err := RemoveSharedRefs(inst, s.SID); err != nil {
return err
}
if err := s.FixRevokedNotes(inst); err != nil {
inst.Logger().WithNamespace("sharing").
Warnf("RevokeByNotification failed to fix notes for revoked sharing %s: %s", s.ID(), err)
}
if rule := s.FirstFilesRule(); rule != nil && rule.Mime == "" {
if err := s.RemoveSharingDir(inst); err != nil {
return err
Expand Down

0 comments on commit 8c60876

Please sign in to comment.