Files
MeBox/internal/service/duplicate_hash.go
T
2026-06-27 10:28:18 +08:00

63 lines
1.5 KiB
Go

// Package service — duplicate sparse file hashing.
package service
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
)
const sampleSize = 1 << 20 // 1 MiB per sample window
// SparseFileHash computes the head+mid+tail SHA-256 of a file, suffixed with
// the file size so two files that happen to collide on the sample window
// but differ in length are still distinguishable.
func SparseFileHash(path string) (string, error) {
if path == "" {
return "", errors.New("empty path")
}
f, err := os.Open(path) // #nosec G304 -- path is selected from configured media library files for duplicate detection.
if err != nil {
return "", err
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return "", err
}
size := st.Size()
h := sha256.New()
if size <= int64(sampleSize)*3 {
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%s-%d", hex.EncodeToString(h.Sum(nil)), size), nil
}
buf := make([]byte, sampleSize)
// head
if _, err := io.ReadFull(f, buf); err != nil {
return "", err
}
h.Write(buf)
// middle
if _, err := f.Seek(size/2-int64(sampleSize)/2, io.SeekStart); err != nil {
return "", err
}
if _, err := io.ReadFull(f, buf); err != nil {
return "", err
}
h.Write(buf)
// tail
if _, err := f.Seek(size-int64(sampleSize), io.SeekStart); err != nil {
return "", err
}
if _, err := io.ReadFull(f, buf); err != nil {
return "", err
}
h.Write(buf)
return fmt.Sprintf("%s-%d", hex.EncodeToString(h.Sum(nil)), size), nil
}