mirror of
https://github.com/kovetskiy/mark.git
synced 2026-03-17 07:57:37 +08:00
The range loop 'for _, attachment := range attachments' copied each element by value. Assigning attachment.Checksum inside the loop only modified the local copy; the original slice element was never updated. All returned attachments had empty Checksum fields, causing every attachment to be treated as changed on every run (the checksum comparison would never match). Switch to an index-based loop so the checksum is written directly to the slice element. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
314 lines
6.9 KiB
Go
314 lines
6.9 KiB
Go
package attachment
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"image"
|
|
_ "image/gif"
|
|
_ "image/jpeg"
|
|
_ "image/png"
|
|
"io"
|
|
"net/url"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/kovetskiy/mark/confluence"
|
|
"github.com/kovetskiy/mark/vfs"
|
|
"github.com/reconquest/karma-go"
|
|
"github.com/reconquest/pkg/log"
|
|
)
|
|
|
|
const (
|
|
AttachmentChecksumPrefix = `mark:checksum: `
|
|
)
|
|
|
|
type Attachment struct {
|
|
ID string
|
|
Name string
|
|
Filename string
|
|
FileBytes []byte
|
|
Checksum string
|
|
Link string
|
|
Width string
|
|
Height string
|
|
Replace string
|
|
}
|
|
|
|
type Attacher interface {
|
|
Attach(Attachment)
|
|
}
|
|
|
|
func ResolveAttachments(
|
|
api *confluence.API,
|
|
page *confluence.PageInfo,
|
|
attachments []Attachment,
|
|
) ([]Attachment, error) {
|
|
for i := range attachments {
|
|
checksum, err := GetChecksum(bytes.NewReader(attachments[i].FileBytes))
|
|
if err != nil {
|
|
return nil, karma.Format(
|
|
err,
|
|
"unable to get checksum for attachment: %q", attachments[i].Name,
|
|
)
|
|
}
|
|
|
|
attachments[i].Checksum = checksum
|
|
}
|
|
|
|
remotes, err := api.GetAttachments(page.ID)
|
|
if err != nil {
|
|
return nil, karma.Format(err, "unable to get attachments for page %s", page.ID)
|
|
}
|
|
|
|
existing := []Attachment{}
|
|
creating := []Attachment{}
|
|
updating := []Attachment{}
|
|
for _, attachment := range attachments {
|
|
var found bool
|
|
var same bool
|
|
for _, remote := range remotes {
|
|
if remote.Filename == attachment.Filename {
|
|
same = attachment.Checksum == strings.TrimPrefix(
|
|
remote.Metadata.Comment,
|
|
AttachmentChecksumPrefix,
|
|
)
|
|
|
|
attachment.ID = remote.ID
|
|
attachment.Link = path.Join(
|
|
remote.Links.Context,
|
|
remote.Links.Download,
|
|
)
|
|
|
|
found = true
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if found {
|
|
if same {
|
|
existing = append(existing, attachment)
|
|
} else {
|
|
updating = append(updating, attachment)
|
|
}
|
|
} else {
|
|
creating = append(creating, attachment)
|
|
}
|
|
}
|
|
|
|
for i, attachment := range creating {
|
|
log.Infof(nil, "creating attachment: %q", attachment.Name)
|
|
|
|
info, err := api.CreateAttachment(
|
|
page.ID,
|
|
attachment.Filename,
|
|
AttachmentChecksumPrefix+attachment.Checksum,
|
|
bytes.NewReader(attachment.FileBytes),
|
|
)
|
|
if err != nil {
|
|
return nil, karma.Format(
|
|
err,
|
|
"unable to create attachment %q",
|
|
attachment.Name,
|
|
)
|
|
}
|
|
|
|
attachment.ID = info.ID
|
|
attachment.Link = path.Join(
|
|
info.Links.Context,
|
|
info.Links.Download,
|
|
)
|
|
|
|
creating[i] = attachment
|
|
}
|
|
|
|
for i, attachment := range updating {
|
|
log.Infof(nil, "updating attachment: %q", attachment.Name)
|
|
|
|
info, err := api.UpdateAttachment(
|
|
page.ID,
|
|
attachment.ID,
|
|
attachment.Filename,
|
|
AttachmentChecksumPrefix+attachment.Checksum,
|
|
bytes.NewReader(attachment.FileBytes),
|
|
)
|
|
if err != nil {
|
|
return nil, karma.Format(
|
|
err,
|
|
"unable to update attachment %q",
|
|
attachment.Name,
|
|
)
|
|
}
|
|
|
|
attachment.Link = path.Join(
|
|
info.Links.Context,
|
|
info.Links.Download,
|
|
)
|
|
|
|
updating[i] = attachment
|
|
}
|
|
|
|
for i := range existing {
|
|
log.Infof(nil, "keeping unmodified attachment: %q", existing[i].Name)
|
|
}
|
|
|
|
attachments = []Attachment{}
|
|
attachments = append(attachments, existing...)
|
|
attachments = append(attachments, creating...)
|
|
attachments = append(attachments, updating...)
|
|
|
|
return attachments, nil
|
|
}
|
|
|
|
func ResolveLocalAttachments(opener vfs.Opener, base string, replacements []string) ([]Attachment, error) {
|
|
attachments, err := prepareAttachments(opener, base, replacements)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range attachments {
|
|
checksum, err := GetChecksum(bytes.NewReader(attachments[i].FileBytes))
|
|
if err != nil {
|
|
return nil, karma.Format(
|
|
err,
|
|
"unable to get checksum for attachment: %q", attachments[i].Name,
|
|
)
|
|
}
|
|
|
|
attachments[i].Checksum = checksum
|
|
}
|
|
return attachments, err
|
|
}
|
|
|
|
// prepareAttachements creates an array of attachement objects based on an array of filepaths
|
|
func prepareAttachments(opener vfs.Opener, base string, replacements []string) ([]Attachment, error) {
|
|
attachments := []Attachment{}
|
|
for _, name := range replacements {
|
|
attachment, err := prepareAttachment(opener, base, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
attachments = append(attachments, attachment)
|
|
}
|
|
|
|
return attachments, nil
|
|
}
|
|
|
|
// prepareAttachement opens the file, reads its content and creates an attachement object
|
|
func prepareAttachment(opener vfs.Opener, base, name string) (Attachment, error) {
|
|
attachmentPath := filepath.Join(base, name)
|
|
file, err := opener.Open(attachmentPath)
|
|
if err != nil {
|
|
return Attachment{}, karma.Format(err, "unable to open file: %q", attachmentPath)
|
|
}
|
|
defer func() {
|
|
_ = file.Close()
|
|
}()
|
|
|
|
fileBytes, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return Attachment{}, karma.Format(err, "unable to read file: %q", attachmentPath)
|
|
}
|
|
|
|
attachment := Attachment{
|
|
Name: name,
|
|
Filename: strings.ReplaceAll(name, "/", "_"),
|
|
FileBytes: fileBytes,
|
|
Replace: name,
|
|
}
|
|
|
|
// Try to detect image dimensions if it's an image attachment
|
|
ext := strings.ToLower(filepath.Ext(name))
|
|
switch ext {
|
|
case ".jpg", ".jpeg", ".png", ".gif":
|
|
if config, _, err := image.DecodeConfig(bytes.NewReader(fileBytes)); err == nil {
|
|
attachment.Width = strconv.Itoa(config.Width)
|
|
attachment.Height = strconv.Itoa(config.Height)
|
|
}
|
|
}
|
|
|
|
return attachment, nil
|
|
}
|
|
|
|
func CompileAttachmentLinks(markdown []byte, attachments []Attachment) []byte {
|
|
links := map[string]string{}
|
|
replaces := []string{}
|
|
|
|
for _, attachment := range attachments {
|
|
links[attachment.Replace] = parseAttachmentLink(attachment.Link)
|
|
replaces = append(replaces, attachment.Replace)
|
|
}
|
|
|
|
// sort by length so first items will have bigger length
|
|
// it's helpful for replacing in case of following names
|
|
// attachments/a.jpg
|
|
// attachments/a.jpg.jpg
|
|
// so we replace longer and then shorter
|
|
sort.SliceStable(replaces, func(i, j int) bool {
|
|
return len(replaces[i]) > len(replaces[j])
|
|
})
|
|
|
|
for _, replace := range replaces {
|
|
to := links[replace]
|
|
|
|
found := false
|
|
if bytes.Contains(markdown, []byte("attachment://"+replace)) {
|
|
from := "attachment://" + replace
|
|
|
|
log.Debugf(nil, "replacing legacy link: %q -> %q", from, to)
|
|
|
|
markdown = bytes.ReplaceAll(
|
|
markdown,
|
|
[]byte(from),
|
|
[]byte(to),
|
|
)
|
|
|
|
found = true
|
|
}
|
|
|
|
if bytes.Contains(markdown, []byte(replace)) {
|
|
from := replace
|
|
|
|
log.Debugf(nil, "replacing link: %q -> %q", from, to)
|
|
|
|
markdown = bytes.ReplaceAll(
|
|
markdown,
|
|
[]byte(from),
|
|
[]byte(to),
|
|
)
|
|
|
|
found = true
|
|
}
|
|
|
|
if !found {
|
|
log.Warningf(nil, "unused attachment: %s", replace)
|
|
}
|
|
}
|
|
|
|
return markdown
|
|
}
|
|
|
|
func GetChecksum(reader io.Reader) (string, error) {
|
|
hash := sha256.New()
|
|
if _, err := io.Copy(hash, reader); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
func parseAttachmentLink(attachLink string) string {
|
|
uri, err := url.ParseRequestURI(attachLink)
|
|
if err != nil {
|
|
return strings.ReplaceAll(attachLink, "&", "&")
|
|
} else {
|
|
return uri.Path +
|
|
"?" + url.QueryEscape(uri.Query().Encode())
|
|
}
|
|
}
|