feat: implement cross-protocol checksum verification for file transfers
This commit is contained in:
@@ -21,4 +21,5 @@ type Explorer interface {
|
||||
Rename(src, dst string) error
|
||||
ReadFile(path string) (io.ReadCloser, error)
|
||||
WriteFile(path string, r io.Reader, size int64) error
|
||||
Checksum(path string) (string, error)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
@@ -177,3 +179,16 @@ func (e *FTPExplorer) ReadFile(path string) (io.ReadCloser, error) {
|
||||
func (e *FTPExplorer) WriteFile(path string, r io.Reader, size int64) error {
|
||||
return e.client.Stor(path, r)
|
||||
}
|
||||
|
||||
func (e *FTPExplorer) Checksum(path string) (string, error) {
|
||||
r, err := e.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer r.Close()
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, r); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -82,6 +84,19 @@ func (e *LocalExplorer) Rename(src, dst string) error {
|
||||
return os.Rename(src, dst)
|
||||
}
|
||||
|
||||
func (e *LocalExplorer) Checksum(path string) (string, error) {
|
||||
r, err := e.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer r.Close()
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, r); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func (e *LocalExplorer) ReadFile(path string) (io.ReadCloser, error) {
|
||||
return os.Open(path)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
package nfs
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
@@ -125,3 +127,16 @@ func (e *NFSExplorer) WriteFile(path string, r io.Reader, size int64) error {
|
||||
_, err = io.Copy(f, r)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *NFSExplorer) Checksum(path string) (string, error) {
|
||||
r, err := e.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer r.Close()
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, r); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
@@ -25,3 +25,4 @@ func (e *NFSExplorer) Delete(path string) error { return nil }
|
||||
func (e *NFSExplorer) Rename(src, dst string) error { return nil }
|
||||
func (e *NFSExplorer) ReadFile(path string) (io.ReadCloser, error) { return nil, nil }
|
||||
func (e *NFSExplorer) WriteFile(path string, r io.Reader, size int64) error { return nil }
|
||||
func (e *NFSExplorer) Checksum(path string) (string, error) { return "", nil }
|
||||
|
||||
@@ -307,3 +307,28 @@ func (e *S3Explorer) WriteFile(path string, r io.Reader, size int64) error {
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *S3Explorer) Checksum(path string) (string, error) {
|
||||
bucket, subpath, _, err := e.getBucketAndPath(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if subpath == "" {
|
||||
return "", fmt.Errorf("cannot calculate checksum of bucket")
|
||||
}
|
||||
|
||||
input := &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(subpath),
|
||||
}
|
||||
head, err := e.client.HeadObject(context.TODO(), input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if head.ETag != nil {
|
||||
return strings.Trim(*head.ETag, "\""), nil
|
||||
}
|
||||
return "", fmt.Errorf("no etag found")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package sftp
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
@@ -162,3 +164,16 @@ func (e *SFTPExplorer) WriteFile(path string, r io.Reader, size int64) error {
|
||||
_, err = io.Copy(f, r)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *SFTPExplorer) Checksum(path string) (string, error) {
|
||||
r, err := e.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer r.Close()
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, r); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package smb
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -243,3 +245,16 @@ func (e *SMBExplorer) WriteFile(path string, r io.Reader, size int64) error {
|
||||
_, err = io.Copy(f, r)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *SMBExplorer) Checksum(path string) (string, error) {
|
||||
r, err := e.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer r.Close()
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, r); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
@@ -102,3 +104,16 @@ func (e *WebDAVExplorer) ReadFile(path string) (io.ReadCloser, error) {
|
||||
func (e *WebDAVExplorer) WriteFile(path string, r io.Reader, size int64) error {
|
||||
return e.client.WriteStream(path, r, 0644)
|
||||
}
|
||||
|
||||
func (e *WebDAVExplorer) Checksum(path string) (string, error) {
|
||||
r, err := e.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer r.Close()
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, r); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -29,6 +33,7 @@ type Transfer struct {
|
||||
ETA int `json:"eta_seconds"`
|
||||
Status Status `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Verify bool `json:"verify"`
|
||||
|
||||
srcExp explorer.Explorer
|
||||
dstExp explorer.Explorer
|
||||
@@ -81,12 +86,36 @@ func (m *Manager) doTransfer(t *Transfer) error {
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
hash := md5.New()
|
||||
tr := io.TeeReader(r, hash)
|
||||
|
||||
pr := &progressReader{
|
||||
r: r,
|
||||
r: tr,
|
||||
t: t,
|
||||
}
|
||||
|
||||
return t.dstExp.WriteFile(t.Destination, pr, t.BytesTotal)
|
||||
if err := t.dstExp.WriteFile(t.Destination, pr, t.BytesTotal); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.Verify {
|
||||
expectedHash := hex.EncodeToString(hash.Sum(nil))
|
||||
t.Status = StatusActive // Still active during verification
|
||||
|
||||
actualHash, err := t.dstExp.Checksum(t.Destination)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verification failed: could not calculate destination checksum: %w", err)
|
||||
}
|
||||
|
||||
if expectedHash != actualHash {
|
||||
// S3 Multi-part ETags end with -N, we can't easily verify them with local md5
|
||||
if !strings.Contains(actualHash, "-") {
|
||||
return fmt.Errorf("verification failed: checksum mismatch (expected %s, got %s)", expectedHash, actualHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type progressReader struct {
|
||||
@@ -109,7 +138,7 @@ func (pr *progressReader) Read(p []byte) (int, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (m *Manager) QueueTransfer(id, srcPath, dstPath, filename string, size int64, srcExp, dstExp explorer.Explorer) error {
|
||||
func (m *Manager) QueueTransfer(id, srcPath, dstPath, filename string, size int64, srcExp, dstExp explorer.Explorer, verify bool) error {
|
||||
if srcExp == nil || dstExp == nil {
|
||||
return errors.New("invalid explorers")
|
||||
}
|
||||
@@ -121,6 +150,7 @@ func (m *Manager) QueueTransfer(id, srcPath, dstPath, filename string, size int6
|
||||
Filename: filename,
|
||||
BytesTotal: size,
|
||||
Status: StatusQueued,
|
||||
Verify: verify,
|
||||
srcExp: srcExp,
|
||||
dstExp: dstExp,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user