Files
share/src/swarm/chunks.go
T
TZGyn 7936de736d Turn the relay into a rendezvous-only swarm: everyone sends and receives
Peers join a room and the relay just broadcasts membership (id + dialable
address); peers then connect to each other directly, PAKE-handshake per
link using the room codephrase, and exchange 1MB chunks. Resume reuses
croc's zero-block scan to detect already-received chunks, whole-file hash
verifies completion, and every peer keeps reseeding what it has to others
until stopped (--one-time restores exit-on-complete for a quick fetch).
2026-07-20 11:12:18 +08:00

161 lines
3.8 KiB
Go

package swarm
import (
"bytes"
"io"
"os"
"sync"
"github.com/tzgyn/share/src/utils"
)
// ChunkSize is fixed at 1MB: large enough to keep protocol overhead low,
// small enough that resuming a transfer never re-fetches much.
const ChunkSize = 1 << 20
// NumChunks returns how many ChunkSize pieces a file of size bytes has.
func NumChunks(size int64) int64 {
if size <= 0 {
return 0
}
return (size + ChunkSize - 1) / ChunkSize
}
func chunkLen(size, offset int64) int64 {
if offset+ChunkSize > size {
return size - offset
}
return ChunkSize
}
// ChunkStore tracks on-disk chunk presence for one file in the swarm and
// reads/writes it at chunk granularity.
type ChunkStore struct {
mu sync.Mutex
path string
size int64
have map[int64]bool // chunk offset -> present
}
// OpenChunkStore opens (or creates) path as a file of exactly size bytes
// for receiving into, and figures out which chunks are already there. This
// reuses the same "a chunk-sized block that's still all zero bytes hasn't
// arrived yet" scan croc itself uses, so resuming an interrupted transfer
// needs no separate manifest of what's already done.
func OpenChunkStore(path string, size int64) (*ChunkStore, error) {
info, statErr := os.Stat(path)
fresh := statErr != nil || info.Size() != size
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o644)
if err != nil {
return nil, err
}
if fresh {
err = f.Truncate(size)
}
f.Close()
if err != nil {
return nil, err
}
cs := &ChunkStore{path: path, size: size, have: make(map[int64]bool)}
if !fresh {
if err := cs.scanExisting(); err != nil {
return nil, err
}
}
return cs, nil
}
// NewSeedChunkStore wraps an already-complete local file being shared into
// the swarm: no scanning, every chunk is simply present. The file is never
// truncated or modified.
func NewSeedChunkStore(path string, size int64) *ChunkStore {
cs := &ChunkStore{path: path, size: size, have: make(map[int64]bool)}
for offset := int64(0); offset < size; offset += ChunkSize {
cs.have[offset] = true
}
return cs
}
func (cs *ChunkStore) scanExisting() error {
f, err := os.Open(cs.path)
if err != nil {
return err
}
defer f.Close()
zero := make([]byte, ChunkSize)
buf := make([]byte, ChunkSize)
for offset := int64(0); offset < cs.size; offset += ChunkSize {
l := chunkLen(cs.size, offset)
if _, err := f.ReadAt(buf[:l], offset); err != nil && err != io.EOF {
return err
}
if !bytes.Equal(buf[:l], zero[:l]) {
cs.have[offset] = true
}
}
return nil
}
func (cs *ChunkStore) HaveOffsets() []int64 {
cs.mu.Lock()
defer cs.mu.Unlock()
list := make([]int64, 0, len(cs.have))
for offset := range cs.have {
list = append(list, offset)
}
return list
}
func (cs *ChunkStore) HasChunk(offset int64) bool {
cs.mu.Lock()
defer cs.mu.Unlock()
return cs.have[offset]
}
func (cs *ChunkStore) Complete() bool {
cs.mu.Lock()
defer cs.mu.Unlock()
return int64(len(cs.have)) == NumChunks(cs.size)
}
func (cs *ChunkStore) ReadChunk(offset int64) ([]byte, error) {
f, err := os.Open(cs.path)
if err != nil {
return nil, err
}
defer f.Close()
buf := make([]byte, chunkLen(cs.size, offset))
if _, err := f.ReadAt(buf, offset); err != nil && err != io.EOF {
return nil, err
}
return buf, nil
}
func (cs *ChunkStore) WriteChunk(offset int64, data []byte) error {
f, err := os.OpenFile(cs.path, os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteAt(data, offset); err != nil {
return err
}
cs.mu.Lock()
cs.have[offset] = true
cs.mu.Unlock()
return nil
}
// VerifyHash confirms a completed file matches the expected whole-file
// hash from the manifest.
func (cs *ChunkStore) VerifyHash(expected []byte) (bool, error) {
got, err := utils.HashFile(cs.path, "xxhash")
if err != nil {
return false, err
}
return bytes.Equal(got, expected), nil
}