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).
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user