2019-07-27 00:45:27 +00:00
|
|
|
package sectorbuilder
|
|
|
|
|
|
|
|
import (
|
2019-11-08 18:49:36 +00:00
|
|
|
"fmt"
|
2019-09-23 10:50:28 +00:00
|
|
|
"io"
|
2019-12-10 17:11:59 +00:00
|
|
|
"io/ioutil"
|
2019-11-07 18:33:46 +00:00
|
|
|
"os"
|
2019-12-10 17:11:59 +00:00
|
|
|
"path/filepath"
|
2019-11-08 18:49:36 +00:00
|
|
|
"strconv"
|
|
|
|
"sync"
|
2019-11-21 16:10:04 +00:00
|
|
|
"sync/atomic"
|
2019-07-27 00:45:27 +00:00
|
|
|
|
2019-11-27 01:46:17 +00:00
|
|
|
sectorbuilder "github.com/filecoin-project/filecoin-ffi"
|
2019-11-08 18:49:36 +00:00
|
|
|
"github.com/ipfs/go-datastore"
|
2019-08-07 23:22:35 +00:00
|
|
|
logging "github.com/ipfs/go-log"
|
2019-12-08 21:53:48 +00:00
|
|
|
dcopy "github.com/otiai10/copy"
|
2019-11-04 17:36:29 +00:00
|
|
|
"golang.org/x/xerrors"
|
2019-08-06 22:04:21 +00:00
|
|
|
|
2019-11-25 16:16:18 +00:00
|
|
|
"github.com/filecoin-project/lotus/build"
|
2019-10-18 04:47:41 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/address"
|
2019-12-09 14:15:25 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/types"
|
2019-11-08 18:49:36 +00:00
|
|
|
"github.com/filecoin-project/lotus/node/modules/dtypes"
|
2019-07-27 00:45:27 +00:00
|
|
|
)
|
|
|
|
|
2019-11-04 17:36:29 +00:00
|
|
|
const PoStReservedWorkers = 1
|
2019-12-05 11:52:13 +00:00
|
|
|
const PoRepProofPartitions = 10
|
2019-11-04 17:36:29 +00:00
|
|
|
|
2019-12-01 04:02:52 +00:00
|
|
|
var lastSectorIdKey = datastore.NewKey("/sectorbuilder/last")
|
2019-11-08 18:49:36 +00:00
|
|
|
|
2019-08-07 23:22:35 +00:00
|
|
|
var log = logging.Logger("sectorbuilder")
|
|
|
|
|
2019-11-25 04:45:13 +00:00
|
|
|
type SortedPublicSectorInfo = sectorbuilder.SortedPublicSectorInfo
|
|
|
|
type SortedPrivateSectorInfo = sectorbuilder.SortedPrivateSectorInfo
|
2019-09-19 14:42:50 +00:00
|
|
|
|
2019-10-27 08:56:53 +00:00
|
|
|
type SealTicket = sectorbuilder.SealTicket
|
|
|
|
|
2019-10-30 18:10:29 +00:00
|
|
|
type SealSeed = sectorbuilder.SealSeed
|
|
|
|
|
2019-10-31 01:22:50 +00:00
|
|
|
type SealPreCommitOutput = sectorbuilder.SealPreCommitOutput
|
|
|
|
|
2019-10-30 18:10:29 +00:00
|
|
|
type SealCommitOutput = sectorbuilder.SealCommitOutput
|
|
|
|
|
|
|
|
type PublicPieceInfo = sectorbuilder.PublicPieceInfo
|
2019-10-27 08:56:53 +00:00
|
|
|
|
2019-11-22 15:48:02 +00:00
|
|
|
type RawSealPreCommitOutput sectorbuilder.RawSealPreCommitOutput
|
2019-11-07 16:39:27 +00:00
|
|
|
|
2019-11-21 22:21:45 +00:00
|
|
|
type EPostCandidate = sectorbuilder.Candidate
|
|
|
|
|
2019-07-27 00:45:27 +00:00
|
|
|
const CommLen = sectorbuilder.CommitmentBytesLen
|
|
|
|
|
2019-12-06 00:27:32 +00:00
|
|
|
type WorkerCfg struct {
|
|
|
|
NoPreCommit bool
|
|
|
|
NoCommit bool
|
|
|
|
|
|
|
|
// TODO: 'cost' info, probably in terms of sealing + transfer speed
|
|
|
|
}
|
|
|
|
|
2019-07-27 00:45:27 +00:00
|
|
|
type SectorBuilder struct {
|
2019-11-26 22:13:01 +00:00
|
|
|
ds dtypes.MetadataDS
|
|
|
|
idLk sync.Mutex
|
2019-11-08 18:49:36 +00:00
|
|
|
|
2019-11-26 22:13:01 +00:00
|
|
|
ssize uint64
|
|
|
|
lastID uint64
|
2019-11-04 17:36:29 +00:00
|
|
|
|
2019-11-05 16:38:54 +00:00
|
|
|
Miner address.Address
|
2019-11-05 17:53:19 +00:00
|
|
|
|
2019-12-01 17:58:31 +00:00
|
|
|
stagedDir string
|
|
|
|
sealedDir string
|
|
|
|
cacheDir string
|
|
|
|
unsealedDir string
|
|
|
|
|
|
|
|
unsealLk sync.Mutex
|
2019-11-07 16:39:27 +00:00
|
|
|
|
2019-12-06 00:27:32 +00:00
|
|
|
noCommit bool
|
|
|
|
noPreCommit bool
|
|
|
|
rateLimit chan struct{}
|
2019-11-21 00:52:59 +00:00
|
|
|
|
2019-12-06 00:27:32 +00:00
|
|
|
precommitTasks chan workerCall
|
|
|
|
commitTasks chan workerCall
|
2019-11-21 00:52:59 +00:00
|
|
|
|
2019-11-21 16:10:04 +00:00
|
|
|
taskCtr uint64
|
2019-11-21 00:52:59 +00:00
|
|
|
remoteLk sync.Mutex
|
2019-11-21 18:38:43 +00:00
|
|
|
remoteCtr int
|
|
|
|
remotes map[int]*remote
|
2019-11-21 00:52:59 +00:00
|
|
|
remoteResults map[uint64]chan<- SealRes
|
|
|
|
|
2019-12-06 00:27:32 +00:00
|
|
|
addPieceWait int32
|
|
|
|
preCommitWait int32
|
|
|
|
commitWait int32
|
|
|
|
unsealWait int32
|
|
|
|
|
2019-11-21 00:52:59 +00:00
|
|
|
stopping chan struct{}
|
|
|
|
}
|
|
|
|
|
2019-11-22 15:48:02 +00:00
|
|
|
type JsonRSPCO struct {
|
2019-12-06 00:27:32 +00:00
|
|
|
CommD []byte
|
|
|
|
CommR []byte
|
2019-11-22 15:48:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (rspco *RawSealPreCommitOutput) ToJson() JsonRSPCO {
|
|
|
|
return JsonRSPCO{
|
2019-12-06 00:27:32 +00:00
|
|
|
CommD: rspco.CommD[:],
|
|
|
|
CommR: rspco.CommR[:],
|
2019-11-22 15:48:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rspco *JsonRSPCO) rspco() RawSealPreCommitOutput {
|
|
|
|
var out RawSealPreCommitOutput
|
|
|
|
copy(out.CommD[:], rspco.CommD)
|
|
|
|
copy(out.CommR[:], rspco.CommR)
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
2019-11-21 00:52:59 +00:00
|
|
|
type SealRes struct {
|
2019-12-04 16:53:32 +00:00
|
|
|
Err string
|
2019-11-30 13:22:50 +00:00
|
|
|
GoErr error `json:"-"`
|
2019-11-21 00:52:59 +00:00
|
|
|
|
2019-11-22 15:48:02 +00:00
|
|
|
Proof []byte
|
|
|
|
Rspco JsonRSPCO
|
2019-11-21 00:52:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type remote struct {
|
|
|
|
lk sync.Mutex
|
|
|
|
|
|
|
|
sealTasks chan<- WorkerTask
|
2019-11-21 14:10:51 +00:00
|
|
|
busy uint64 // only for metrics
|
2019-07-27 00:45:27 +00:00
|
|
|
}
|
|
|
|
|
2019-11-04 16:47:08 +00:00
|
|
|
type Config struct {
|
|
|
|
SectorSize uint64
|
|
|
|
Miner address.Address
|
|
|
|
|
2019-12-06 17:53:33 +00:00
|
|
|
WorkerThreads uint8
|
2019-12-06 21:49:44 +00:00
|
|
|
FallbackLastID uint64
|
2019-12-06 00:27:32 +00:00
|
|
|
NoCommit bool
|
|
|
|
NoPreCommit bool
|
2019-11-04 16:47:08 +00:00
|
|
|
|
2019-10-31 01:22:50 +00:00
|
|
|
CacheDir string
|
2019-07-27 00:45:27 +00:00
|
|
|
SealedDir string
|
|
|
|
StagedDir string
|
2019-12-01 17:58:31 +00:00
|
|
|
UnsealedDir string
|
2019-12-06 17:53:33 +00:00
|
|
|
_ struct{} // guard against nameless init
|
2019-07-27 00:45:27 +00:00
|
|
|
}
|
|
|
|
|
2019-11-08 20:30:50 +00:00
|
|
|
func New(cfg *Config, ds dtypes.MetadataDS) (*SectorBuilder, error) {
|
2019-11-21 00:52:59 +00:00
|
|
|
if cfg.WorkerThreads < PoStReservedWorkers {
|
|
|
|
return nil, xerrors.Errorf("minimum worker threads is %d, specified %d", PoStReservedWorkers, cfg.WorkerThreads)
|
2019-11-04 17:36:29 +00:00
|
|
|
}
|
|
|
|
|
2019-12-01 17:58:31 +00:00
|
|
|
for _, dir := range []string{cfg.StagedDir, cfg.SealedDir, cfg.CacheDir, cfg.UnsealedDir} {
|
2019-11-07 18:33:46 +00:00
|
|
|
if err := os.Mkdir(dir, 0755); err != nil {
|
|
|
|
if os.IsExist(err) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-08 18:49:36 +00:00
|
|
|
var lastUsedID uint64
|
2019-12-06 21:49:44 +00:00
|
|
|
b, err := ds.Get(lastSectorIdKey)
|
|
|
|
switch err {
|
|
|
|
case nil:
|
|
|
|
i, err := strconv.ParseInt(string(b), 10, 64)
|
|
|
|
if err != nil {
|
2019-11-08 18:49:36 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2019-12-06 21:49:44 +00:00
|
|
|
lastUsedID = uint64(i)
|
|
|
|
case datastore.ErrNotFound:
|
|
|
|
lastUsedID = cfg.FallbackLastID
|
|
|
|
default:
|
|
|
|
return nil, err
|
2019-11-08 18:49:36 +00:00
|
|
|
}
|
|
|
|
|
2019-11-21 00:52:59 +00:00
|
|
|
rlimit := cfg.WorkerThreads - PoStReservedWorkers
|
|
|
|
|
|
|
|
sealLocal := rlimit > 0
|
|
|
|
|
2019-12-05 15:53:29 +00:00
|
|
|
if rlimit == 0 {
|
|
|
|
rlimit = 1
|
|
|
|
}
|
|
|
|
|
2019-11-08 18:49:36 +00:00
|
|
|
sb := &SectorBuilder{
|
2019-11-26 22:13:01 +00:00
|
|
|
ds: ds,
|
2019-11-08 18:49:36 +00:00
|
|
|
|
2019-11-26 22:13:01 +00:00
|
|
|
ssize: cfg.SectorSize,
|
|
|
|
lastID: lastUsedID,
|
2019-11-06 23:09:48 +00:00
|
|
|
|
2019-12-01 21:22:03 +00:00
|
|
|
stagedDir: cfg.StagedDir,
|
|
|
|
sealedDir: cfg.SealedDir,
|
|
|
|
cacheDir: cfg.CacheDir,
|
|
|
|
unsealedDir: cfg.UnsealedDir,
|
2019-11-07 16:39:27 +00:00
|
|
|
|
2019-11-21 00:52:59 +00:00
|
|
|
Miner: cfg.Miner,
|
|
|
|
|
2019-12-06 00:27:32 +00:00
|
|
|
noPreCommit: cfg.NoPreCommit || !sealLocal,
|
|
|
|
noCommit: cfg.NoCommit || !sealLocal,
|
|
|
|
rateLimit: make(chan struct{}, rlimit),
|
2019-11-21 00:52:59 +00:00
|
|
|
|
2019-12-06 00:27:32 +00:00
|
|
|
taskCtr: 1,
|
|
|
|
precommitTasks: make(chan workerCall),
|
|
|
|
commitTasks: make(chan workerCall),
|
|
|
|
remoteResults: map[uint64]chan<- SealRes{},
|
|
|
|
remotes: map[int]*remote{},
|
2019-11-21 14:10:51 +00:00
|
|
|
|
|
|
|
stopping: make(chan struct{}),
|
2019-11-08 18:49:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return sb, nil
|
2019-07-27 00:45:27 +00:00
|
|
|
}
|
|
|
|
|
2019-11-21 14:10:51 +00:00
|
|
|
func NewStandalone(cfg *Config) (*SectorBuilder, error) {
|
2019-12-03 02:23:49 +00:00
|
|
|
for _, dir := range []string{cfg.StagedDir, cfg.SealedDir, cfg.CacheDir, cfg.UnsealedDir} {
|
2019-11-21 18:38:43 +00:00
|
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
2019-11-21 14:10:51 +00:00
|
|
|
if os.IsExist(err) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &SectorBuilder{
|
2019-12-04 16:53:32 +00:00
|
|
|
ds: nil,
|
2019-11-30 08:41:52 +00:00
|
|
|
|
2019-12-04 16:53:32 +00:00
|
|
|
ssize: cfg.SectorSize,
|
2019-11-30 08:41:52 +00:00
|
|
|
|
2019-12-04 16:53:32 +00:00
|
|
|
Miner: cfg.Miner,
|
|
|
|
stagedDir: cfg.StagedDir,
|
|
|
|
sealedDir: cfg.SealedDir,
|
|
|
|
cacheDir: cfg.CacheDir,
|
|
|
|
unsealedDir: cfg.UnsealedDir,
|
2019-11-21 16:10:04 +00:00
|
|
|
|
|
|
|
taskCtr: 1,
|
2019-11-21 18:38:43 +00:00
|
|
|
remotes: map[int]*remote{},
|
2019-11-21 14:10:51 +00:00
|
|
|
rateLimit: make(chan struct{}, cfg.WorkerThreads),
|
|
|
|
stopping: make(chan struct{}),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2019-11-21 16:10:04 +00:00
|
|
|
func (sb *SectorBuilder) checkRateLimit() {
|
2019-11-04 17:36:29 +00:00
|
|
|
if cap(sb.rateLimit) == len(sb.rateLimit) {
|
2019-11-21 16:10:04 +00:00
|
|
|
log.Warn("rate-limiting local sectorbuilder call")
|
2019-11-04 17:36:29 +00:00
|
|
|
}
|
2019-11-21 16:10:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (sb *SectorBuilder) RateLimit() func() {
|
|
|
|
sb.checkRateLimit()
|
|
|
|
|
2019-11-04 17:36:29 +00:00
|
|
|
sb.rateLimit <- struct{}{}
|
|
|
|
|
|
|
|
return func() {
|
|
|
|
<-sb.rateLimit
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-21 16:23:42 +00:00
|
|
|
type WorkerStats struct {
|
2019-11-21 18:38:43 +00:00
|
|
|
LocalFree int
|
2019-11-21 16:23:42 +00:00
|
|
|
LocalReserved int
|
2019-11-21 18:38:43 +00:00
|
|
|
LocalTotal int
|
2019-11-21 16:23:42 +00:00
|
|
|
// todo: post in progress
|
|
|
|
RemotesTotal int
|
2019-11-21 18:38:43 +00:00
|
|
|
RemotesFree int
|
2019-12-06 00:27:32 +00:00
|
|
|
|
|
|
|
AddPieceWait int
|
|
|
|
PreCommitWait int
|
|
|
|
CommitWait int
|
|
|
|
UnsealWait int
|
2019-11-21 16:23:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (sb *SectorBuilder) WorkerStats() WorkerStats {
|
|
|
|
sb.remoteLk.Lock()
|
|
|
|
defer sb.remoteLk.Unlock()
|
|
|
|
|
|
|
|
remoteFree := len(sb.remotes)
|
|
|
|
for _, r := range sb.remotes {
|
|
|
|
if r.busy > 0 {
|
|
|
|
remoteFree--
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return WorkerStats{
|
|
|
|
LocalFree: cap(sb.rateLimit) - len(sb.rateLimit),
|
|
|
|
LocalReserved: PoStReservedWorkers,
|
|
|
|
LocalTotal: cap(sb.rateLimit) + PoStReservedWorkers,
|
|
|
|
RemotesTotal: len(sb.remotes),
|
|
|
|
RemotesFree: remoteFree,
|
2019-12-06 00:27:32 +00:00
|
|
|
|
|
|
|
AddPieceWait: int(atomic.LoadInt32(&sb.addPieceWait)),
|
|
|
|
PreCommitWait: int(atomic.LoadInt32(&sb.preCommitWait)),
|
|
|
|
CommitWait: int(atomic.LoadInt32(&sb.commitWait)),
|
|
|
|
UnsealWait: int(atomic.LoadInt32(&sb.unsealWait)),
|
2019-11-21 16:23:42 +00:00
|
|
|
}
|
2019-11-08 18:15:13 +00:00
|
|
|
}
|
|
|
|
|
2019-10-21 11:58:41 +00:00
|
|
|
func addressToProverID(a address.Address) [32]byte {
|
|
|
|
var proverId [32]byte
|
2019-08-07 06:35:57 +00:00
|
|
|
copy(proverId[:], a.Payload())
|
|
|
|
return proverId
|
|
|
|
}
|
|
|
|
|
2019-11-07 16:39:27 +00:00
|
|
|
func (sb *SectorBuilder) AcquireSectorId() (uint64, error) {
|
2019-11-08 18:49:36 +00:00
|
|
|
sb.idLk.Lock()
|
|
|
|
defer sb.idLk.Unlock()
|
|
|
|
|
2019-11-26 22:13:01 +00:00
|
|
|
sb.lastID++
|
|
|
|
id := sb.lastID
|
|
|
|
|
2019-12-01 04:02:52 +00:00
|
|
|
err := sb.ds.Put(lastSectorIdKey, []byte(fmt.Sprint(id)))
|
2019-11-08 18:49:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return id, nil
|
2019-11-07 16:39:27 +00:00
|
|
|
}
|
|
|
|
|
2019-11-07 20:40:46 +00:00
|
|
|
func (sb *SectorBuilder) AddPiece(pieceSize uint64, sectorId uint64, file io.Reader, existingPieceSizes []uint64) (PublicPieceInfo, error) {
|
2019-12-06 00:27:32 +00:00
|
|
|
atomic.AddInt32(&sb.addPieceWait, 1)
|
2019-11-20 17:28:14 +00:00
|
|
|
ret := sb.RateLimit()
|
2019-12-06 00:27:32 +00:00
|
|
|
atomic.AddInt32(&sb.addPieceWait, -1)
|
2019-11-20 17:28:14 +00:00
|
|
|
defer ret()
|
|
|
|
|
2019-09-23 10:50:28 +00:00
|
|
|
f, werr, err := toReadableFile(file, int64(pieceSize))
|
|
|
|
if err != nil {
|
2019-11-07 16:39:27 +00:00
|
|
|
return PublicPieceInfo{}, err
|
2019-09-23 10:50:28 +00:00
|
|
|
}
|
|
|
|
|
2019-11-07 16:39:27 +00:00
|
|
|
stagedFile, err := sb.stagedSectorFile(sectorId)
|
2019-09-23 10:50:28 +00:00
|
|
|
if err != nil {
|
2019-11-07 16:39:27 +00:00
|
|
|
return PublicPieceInfo{}, err
|
|
|
|
}
|
|
|
|
|
2019-11-27 01:46:17 +00:00
|
|
|
_, _, commP, err := sectorbuilder.WriteWithAlignment(f, pieceSize, stagedFile, existingPieceSizes)
|
2019-11-07 16:39:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return PublicPieceInfo{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := stagedFile.Close(); err != nil {
|
|
|
|
return PublicPieceInfo{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := f.Close(); err != nil {
|
|
|
|
return PublicPieceInfo{}, err
|
2019-09-23 10:50:28 +00:00
|
|
|
}
|
|
|
|
|
2019-11-07 16:39:27 +00:00
|
|
|
return PublicPieceInfo{
|
|
|
|
Size: pieceSize,
|
|
|
|
CommP: commP,
|
|
|
|
}, werr()
|
2019-07-27 00:45:27 +00:00
|
|
|
}
|
|
|
|
|
2019-12-01 17:58:31 +00:00
|
|
|
func (sb *SectorBuilder) ReadPieceFromSealedSector(sectorID uint64, offset uint64, size uint64, ticket []byte, commD []byte) (io.ReadCloser, error) {
|
2019-12-06 00:27:32 +00:00
|
|
|
atomic.AddInt32(&sb.unsealWait, 1)
|
|
|
|
// TODO: Don't wait if cached
|
2019-12-01 17:58:31 +00:00
|
|
|
ret := sb.RateLimit() // TODO: check perf, consider remote unseal worker
|
2019-11-04 17:36:29 +00:00
|
|
|
defer ret()
|
2019-12-06 00:27:32 +00:00
|
|
|
atomic.AddInt32(&sb.unsealWait, -1)
|
2019-11-04 17:36:29 +00:00
|
|
|
|
2019-12-01 17:58:31 +00:00
|
|
|
sb.unsealLk.Lock() // TODO: allow unsealing unrelated sectors in parallel
|
2019-12-01 20:07:42 +00:00
|
|
|
defer sb.unsealLk.Unlock()
|
2019-12-01 17:58:31 +00:00
|
|
|
|
|
|
|
cacheDir, err := sb.sectorCacheDir(sectorID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-12-03 02:23:49 +00:00
|
|
|
sealedPath, err := sb.SealedSectorPath(sectorID)
|
2019-12-01 17:58:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
unsealedPath := sb.unsealedSectorPath(sectorID)
|
|
|
|
|
|
|
|
// TODO: GC for those
|
|
|
|
// (Probably configurable count of sectors to be kept unsealed, and just
|
|
|
|
// remove last used one (or use whatever other cache policy makes sense))
|
|
|
|
f, err := os.OpenFile(unsealedPath, os.O_RDONLY, 0644)
|
|
|
|
if err != nil {
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var commd [CommLen]byte
|
|
|
|
copy(commd[:], commD)
|
|
|
|
|
|
|
|
var tkt [CommLen]byte
|
|
|
|
copy(tkt[:], ticket)
|
|
|
|
|
|
|
|
err = sectorbuilder.Unseal(sb.ssize,
|
|
|
|
PoRepProofPartitions,
|
|
|
|
cacheDir,
|
|
|
|
sealedPath,
|
|
|
|
unsealedPath,
|
|
|
|
sectorID,
|
|
|
|
addressToProverID(sb.Miner),
|
|
|
|
tkt,
|
|
|
|
commd)
|
|
|
|
if err != nil {
|
|
|
|
return nil, xerrors.Errorf("unseal failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
f, err = os.OpenFile(unsealedPath, os.O_RDONLY, 0644)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := f.Seek(int64(offset), io.SeekStart); err != nil {
|
|
|
|
return nil, xerrors.Errorf("seek: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
lr := io.LimitReader(f, int64(size))
|
|
|
|
|
|
|
|
return &struct {
|
|
|
|
io.Reader
|
|
|
|
io.Closer
|
|
|
|
}{
|
|
|
|
Reader: lr,
|
|
|
|
Closer: f,
|
|
|
|
}, nil
|
2019-07-27 00:45:27 +00:00
|
|
|
}
|
|
|
|
|
2019-11-21 16:10:04 +00:00
|
|
|
func (sb *SectorBuilder) sealPreCommitRemote(call workerCall) (RawSealPreCommitOutput, error) {
|
2019-12-06 00:27:32 +00:00
|
|
|
atomic.AddInt32(&sb.preCommitWait, -1)
|
|
|
|
|
2019-11-21 16:10:04 +00:00
|
|
|
select {
|
|
|
|
case ret := <-call.ret:
|
2019-11-30 09:25:31 +00:00
|
|
|
var err error
|
|
|
|
if ret.Err != "" {
|
|
|
|
err = xerrors.New(ret.Err)
|
|
|
|
}
|
|
|
|
return ret.Rspco.rspco(), err
|
2019-11-21 16:10:04 +00:00
|
|
|
case <-sb.stopping:
|
|
|
|
return RawSealPreCommitOutput{}, xerrors.New("sectorbuilder stopped")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-07 16:39:27 +00:00
|
|
|
func (sb *SectorBuilder) SealPreCommit(sectorID uint64, ticket SealTicket, pieces []PublicPieceInfo) (RawSealPreCommitOutput, error) {
|
2019-11-21 16:10:04 +00:00
|
|
|
call := workerCall{
|
|
|
|
task: WorkerTask{
|
|
|
|
Type: WorkerPreCommit,
|
|
|
|
TaskID: atomic.AddUint64(&sb.taskCtr, 1),
|
|
|
|
SectorID: sectorID,
|
|
|
|
SealTicket: ticket,
|
|
|
|
Pieces: pieces,
|
|
|
|
},
|
|
|
|
ret: make(chan SealRes),
|
|
|
|
}
|
|
|
|
|
2019-12-06 00:27:32 +00:00
|
|
|
atomic.AddInt32(&sb.preCommitWait, 1)
|
|
|
|
|
2019-11-21 16:10:04 +00:00
|
|
|
select { // prefer remote
|
2019-12-06 00:27:32 +00:00
|
|
|
case sb.precommitTasks <- call:
|
2019-11-21 16:10:04 +00:00
|
|
|
return sb.sealPreCommitRemote(call)
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
sb.checkRateLimit()
|
|
|
|
|
2019-12-05 15:53:29 +00:00
|
|
|
rl := sb.rateLimit
|
2019-12-06 00:27:32 +00:00
|
|
|
if sb.noPreCommit {
|
2019-12-05 15:53:29 +00:00
|
|
|
rl = make(chan struct{})
|
|
|
|
}
|
|
|
|
|
2019-11-21 16:10:04 +00:00
|
|
|
select { // use whichever is available
|
2019-12-06 00:27:32 +00:00
|
|
|
case sb.precommitTasks <- call:
|
2019-11-21 16:10:04 +00:00
|
|
|
return sb.sealPreCommitRemote(call)
|
2019-12-05 15:53:29 +00:00
|
|
|
case rl <- struct{}{}:
|
2019-11-21 16:10:04 +00:00
|
|
|
}
|
|
|
|
|
2019-12-06 00:27:32 +00:00
|
|
|
atomic.AddInt32(&sb.preCommitWait, -1)
|
|
|
|
|
2019-11-21 16:10:04 +00:00
|
|
|
// local
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
<-sb.rateLimit
|
|
|
|
}()
|
2019-11-04 17:36:29 +00:00
|
|
|
|
2019-11-07 16:39:27 +00:00
|
|
|
cacheDir, err := sb.sectorCacheDir(sectorID)
|
|
|
|
if err != nil {
|
2019-11-22 15:48:02 +00:00
|
|
|
return RawSealPreCommitOutput{}, xerrors.Errorf("getting cache dir: %w", err)
|
2019-11-07 16:39:27 +00:00
|
|
|
}
|
2019-10-31 01:22:50 +00:00
|
|
|
|
2019-11-21 00:52:59 +00:00
|
|
|
sealedPath, err := sb.SealedSectorPath(sectorID)
|
2019-11-07 16:39:27 +00:00
|
|
|
if err != nil {
|
2019-11-22 15:48:02 +00:00
|
|
|
return RawSealPreCommitOutput{}, xerrors.Errorf("getting sealed sector path: %w", err)
|
2019-11-07 16:39:27 +00:00
|
|
|
}
|
2019-11-04 17:36:29 +00:00
|
|
|
|
2019-12-10 19:27:57 +00:00
|
|
|
e, err := os.OpenFile(sealedPath, os.O_RDWR|os.O_CREATE, 0644)
|
|
|
|
if err != nil {
|
|
|
|
return RawSealPreCommitOutput{}, xerrors.Errorf("ensuring sealed file exists: %w", err)
|
|
|
|
}
|
|
|
|
if err := e.Close(); err != nil {
|
|
|
|
return RawSealPreCommitOutput{}, err
|
|
|
|
}
|
|
|
|
|
2019-11-07 19:54:24 +00:00
|
|
|
var sum uint64
|
|
|
|
for _, piece := range pieces {
|
|
|
|
sum += piece.Size
|
|
|
|
}
|
|
|
|
ussize := UserBytesForSectorSize(sb.ssize)
|
|
|
|
if sum != ussize {
|
2019-11-07 20:40:46 +00:00
|
|
|
return RawSealPreCommitOutput{}, xerrors.Errorf("aggregated piece sizes don't match sector size: %d != %d (%d)", sum, ussize, int64(ussize-sum))
|
2019-11-07 19:54:24 +00:00
|
|
|
}
|
|
|
|
|
2019-11-21 00:52:59 +00:00
|
|
|
stagedPath := sb.StagedSectorPath(sectorID)
|
2019-11-07 19:54:24 +00:00
|
|
|
|
2019-11-27 01:46:17 +00:00
|
|
|
rspco, err := sectorbuilder.SealPreCommit(
|
2019-11-07 16:39:27 +00:00
|
|
|
sb.ssize,
|
|
|
|
PoRepProofPartitions,
|
|
|
|
cacheDir,
|
2019-11-07 19:54:24 +00:00
|
|
|
stagedPath,
|
2019-11-07 16:39:27 +00:00
|
|
|
sealedPath,
|
|
|
|
sectorID,
|
|
|
|
addressToProverID(sb.Miner),
|
|
|
|
ticket.TicketBytes,
|
|
|
|
pieces,
|
|
|
|
)
|
2019-11-07 19:54:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return RawSealPreCommitOutput{}, xerrors.Errorf("presealing sector %d (%s): %w", sectorID, stagedPath, err)
|
|
|
|
}
|
|
|
|
|
2019-11-22 15:48:02 +00:00
|
|
|
return RawSealPreCommitOutput(rspco), nil
|
2019-07-27 00:45:27 +00:00
|
|
|
}
|
|
|
|
|
2019-11-21 19:51:48 +00:00
|
|
|
func (sb *SectorBuilder) sealCommitRemote(call workerCall) (proof []byte, err error) {
|
2019-12-06 00:27:32 +00:00
|
|
|
atomic.AddInt32(&sb.commitWait, -1)
|
|
|
|
|
2019-11-21 19:51:48 +00:00
|
|
|
select {
|
|
|
|
case ret := <-call.ret:
|
2019-11-30 09:25:31 +00:00
|
|
|
if ret.Err != "" {
|
|
|
|
err = xerrors.New(ret.Err)
|
|
|
|
}
|
|
|
|
return ret.Proof, err
|
2019-11-21 19:51:48 +00:00
|
|
|
case <-sb.stopping:
|
|
|
|
return nil, xerrors.New("sectorbuilder stopped")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (sb *SectorBuilder) sealCommitLocal(sectorID uint64, ticket SealTicket, seed SealSeed, pieces []PublicPieceInfo, rspco RawSealPreCommitOutput) (proof []byte, err error) {
|
2019-12-06 00:27:32 +00:00
|
|
|
atomic.AddInt32(&sb.commitWait, -1)
|
|
|
|
|
2019-11-21 19:51:48 +00:00
|
|
|
defer func() {
|
|
|
|
<-sb.rateLimit
|
|
|
|
}()
|
2019-11-04 17:36:29 +00:00
|
|
|
|
2019-11-07 16:39:27 +00:00
|
|
|
cacheDir, err := sb.sectorCacheDir(sectorID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-11-27 01:46:17 +00:00
|
|
|
proof, err = sectorbuilder.SealCommit(
|
2019-11-07 16:39:27 +00:00
|
|
|
sb.ssize,
|
|
|
|
PoRepProofPartitions,
|
|
|
|
cacheDir,
|
|
|
|
sectorID,
|
|
|
|
addressToProverID(sb.Miner),
|
|
|
|
ticket.TicketBytes,
|
|
|
|
seed.TicketBytes,
|
|
|
|
pieces,
|
2019-11-22 15:48:02 +00:00
|
|
|
sectorbuilder.RawSealPreCommitOutput(rspco),
|
2019-11-07 16:39:27 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
2019-11-22 15:48:02 +00:00
|
|
|
log.Warn("StandaloneSealCommit error: ", err)
|
|
|
|
log.Warnf("sid:%d tkt:%v seed:%v, ppi:%v rspco:%v", sectorID, ticket, seed, pieces, rspco)
|
|
|
|
|
2019-11-07 16:39:27 +00:00
|
|
|
return nil, xerrors.Errorf("StandaloneSealCommit: %w", err)
|
|
|
|
}
|
|
|
|
|
2019-11-21 19:51:48 +00:00
|
|
|
return proof, nil
|
|
|
|
}
|
|
|
|
|
2019-11-30 08:41:52 +00:00
|
|
|
func (sb *SectorBuilder) SealCommit(sectorID uint64, ticket SealTicket, seed SealSeed, pieces []PublicPieceInfo, rspco RawSealPreCommitOutput) (proof []byte, err error) {
|
2019-11-21 19:51:48 +00:00
|
|
|
call := workerCall{
|
|
|
|
task: WorkerTask{
|
|
|
|
Type: WorkerCommit,
|
|
|
|
TaskID: atomic.AddUint64(&sb.taskCtr, 1),
|
|
|
|
SectorID: sectorID,
|
|
|
|
SealTicket: ticket,
|
|
|
|
Pieces: pieces,
|
|
|
|
|
|
|
|
SealSeed: seed,
|
|
|
|
Rspco: rspco,
|
|
|
|
},
|
|
|
|
ret: make(chan SealRes),
|
|
|
|
}
|
|
|
|
|
2019-12-06 00:27:32 +00:00
|
|
|
atomic.AddInt32(&sb.commitWait, 1)
|
|
|
|
|
2019-11-21 19:51:48 +00:00
|
|
|
select { // prefer remote
|
2019-12-06 00:27:32 +00:00
|
|
|
case sb.commitTasks <- call:
|
2019-11-21 19:51:48 +00:00
|
|
|
proof, err = sb.sealCommitRemote(call)
|
|
|
|
default:
|
|
|
|
sb.checkRateLimit()
|
|
|
|
|
2019-12-05 15:53:29 +00:00
|
|
|
rl := sb.rateLimit
|
2019-12-06 00:27:32 +00:00
|
|
|
if sb.noCommit {
|
2019-12-05 15:53:29 +00:00
|
|
|
rl = make(chan struct{})
|
|
|
|
}
|
|
|
|
|
2019-11-21 19:51:48 +00:00
|
|
|
select { // use whichever is available
|
2019-12-06 00:27:32 +00:00
|
|
|
case sb.commitTasks <- call:
|
2019-11-21 19:51:48 +00:00
|
|
|
proof, err = sb.sealCommitRemote(call)
|
2019-12-05 15:53:29 +00:00
|
|
|
case rl <- struct{}{}:
|
2019-11-21 19:51:48 +00:00
|
|
|
proof, err = sb.sealCommitLocal(sectorID, ticket, seed, pieces, rspco)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, xerrors.Errorf("commit: %w", err)
|
|
|
|
}
|
|
|
|
|
2019-11-30 08:41:52 +00:00
|
|
|
return proof, nil
|
2019-11-06 23:09:48 +00:00
|
|
|
}
|
2019-11-22 15:48:02 +00:00
|
|
|
|
2019-11-25 04:45:13 +00:00
|
|
|
func (sb *SectorBuilder) ComputeElectionPoSt(sectorInfo SortedPublicSectorInfo, challengeSeed []byte, winners []EPostCandidate) ([]byte, error) {
|
2019-11-21 22:21:45 +00:00
|
|
|
if len(challengeSeed) != CommLen {
|
|
|
|
return nil, xerrors.Errorf("given challenge seed was the wrong length: %d != %d", len(challengeSeed), CommLen)
|
2019-11-07 16:39:27 +00:00
|
|
|
}
|
2019-11-21 22:21:45 +00:00
|
|
|
var cseed [CommLen]byte
|
|
|
|
copy(cseed[:], challengeSeed)
|
2019-11-07 16:39:27 +00:00
|
|
|
|
2019-11-26 02:43:43 +00:00
|
|
|
privsects, err := sb.pubSectorToPriv(sectorInfo)
|
2019-11-07 16:39:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-11-26 02:43:43 +00:00
|
|
|
proverID := addressToProverID(sb.Miner)
|
2019-11-30 23:17:50 +00:00
|
|
|
|
2019-11-27 22:34:48 +00:00
|
|
|
return sectorbuilder.GeneratePoSt(sb.ssize, proverID, privsects, cseed, winners)
|
2019-11-21 22:21:45 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 04:45:13 +00:00
|
|
|
func (sb *SectorBuilder) GenerateEPostCandidates(sectorInfo SortedPublicSectorInfo, challengeSeed [CommLen]byte, faults []uint64) ([]EPostCandidate, error) {
|
|
|
|
privsectors, err := sb.pubSectorToPriv(sectorInfo)
|
2019-11-21 19:51:48 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-12-09 14:15:25 +00:00
|
|
|
challengeCount := types.ElectionPostChallengeCount(uint64(len(sectorInfo.Values())))
|
2019-11-25 16:16:18 +00:00
|
|
|
|
2019-11-25 04:45:13 +00:00
|
|
|
proverID := addressToProverID(sb.Miner)
|
2019-11-27 22:34:48 +00:00
|
|
|
return sectorbuilder.GenerateCandidates(sb.ssize, proverID, challengeSeed, challengeCount, privsectors)
|
2019-11-25 04:45:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (sb *SectorBuilder) pubSectorToPriv(sectorInfo SortedPublicSectorInfo) (SortedPrivateSectorInfo, error) {
|
2019-11-27 01:46:17 +00:00
|
|
|
var out []sectorbuilder.PrivateSectorInfo
|
2019-11-25 04:45:13 +00:00
|
|
|
for _, s := range sectorInfo.Values() {
|
|
|
|
cachePath, err := sb.sectorCacheDir(s.SectorID)
|
|
|
|
if err != nil {
|
|
|
|
return SortedPrivateSectorInfo{}, xerrors.Errorf("getting cache path for sector %d: %w", s.SectorID, err)
|
|
|
|
}
|
|
|
|
|
2019-11-30 08:41:52 +00:00
|
|
|
sealedPath, err := sb.SealedSectorPath(s.SectorID)
|
2019-11-25 04:45:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return SortedPrivateSectorInfo{}, xerrors.Errorf("getting sealed path for sector %d: %w", s.SectorID, err)
|
|
|
|
}
|
|
|
|
|
2019-11-27 01:46:17 +00:00
|
|
|
out = append(out, sectorbuilder.PrivateSectorInfo{
|
2019-11-25 04:45:13 +00:00
|
|
|
SectorID: s.SectorID,
|
|
|
|
CommR: s.CommR,
|
|
|
|
CacheDirPath: cachePath,
|
|
|
|
SealedSectorPath: sealedPath,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return NewSortedPrivateSectorInfo(out), nil
|
2019-11-21 22:21:45 +00:00
|
|
|
}
|
|
|
|
|
2019-11-28 17:44:49 +00:00
|
|
|
func (sb *SectorBuilder) GenerateFallbackPoSt(sectorInfo SortedPublicSectorInfo, challengeSeed [CommLen]byte, faults []uint64) ([]EPostCandidate, []byte, error) {
|
2019-11-28 03:36:34 +00:00
|
|
|
privsectors, err := sb.pubSectorToPriv(sectorInfo)
|
2019-11-07 16:39:27 +00:00
|
|
|
if err != nil {
|
2019-11-28 17:44:49 +00:00
|
|
|
return nil, nil, err
|
2019-11-07 16:39:27 +00:00
|
|
|
}
|
2019-11-28 03:36:34 +00:00
|
|
|
|
2019-11-28 12:46:56 +00:00
|
|
|
challengeCount := fallbackPostChallengeCount(uint64(len(sectorInfo.Values())))
|
2019-11-28 03:36:34 +00:00
|
|
|
|
|
|
|
proverID := addressToProverID(sb.Miner)
|
|
|
|
candidates, err := sectorbuilder.GenerateCandidates(sb.ssize, proverID, challengeSeed, challengeCount, privsectors)
|
|
|
|
if err != nil {
|
2019-11-28 17:44:49 +00:00
|
|
|
return nil, nil, err
|
2019-11-28 03:36:34 +00:00
|
|
|
}
|
|
|
|
|
2019-11-28 17:44:49 +00:00
|
|
|
proof, err := sectorbuilder.GeneratePoSt(sb.ssize, proverID, privsectors, challengeSeed, candidates)
|
|
|
|
return candidates, proof, err
|
2019-10-29 22:19:58 +00:00
|
|
|
}
|
|
|
|
|
2019-11-21 00:52:59 +00:00
|
|
|
func (sb *SectorBuilder) Stop() {
|
|
|
|
close(sb.stopping)
|
2019-11-01 03:57:10 +00:00
|
|
|
}
|
2019-11-25 16:16:18 +00:00
|
|
|
|
2019-11-28 12:46:56 +00:00
|
|
|
func fallbackPostChallengeCount(sectors uint64) uint64 {
|
2019-12-09 14:15:25 +00:00
|
|
|
challengeCount := types.ElectionPostChallengeCount(sectors)
|
2019-11-28 12:46:56 +00:00
|
|
|
if challengeCount > build.MaxFallbackPostChallengeCount {
|
|
|
|
return build.MaxFallbackPostChallengeCount
|
|
|
|
}
|
|
|
|
return challengeCount
|
|
|
|
}
|
2019-11-30 23:17:50 +00:00
|
|
|
|
2019-12-10 17:11:59 +00:00
|
|
|
func (sb *SectorBuilder) ImportFrom(osb *SectorBuilder, symlink bool) error {
|
2019-12-11 02:25:48 +00:00
|
|
|
if err := migrate(osb.cacheDir, sb.cacheDir, symlink); err != nil {
|
2019-11-30 23:17:50 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-12-11 02:25:48 +00:00
|
|
|
if err := migrate(osb.sealedDir, sb.sealedDir, symlink); err != nil {
|
2019-11-30 23:17:50 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-12-11 02:25:48 +00:00
|
|
|
if err := migrate(osb.stagedDir, sb.stagedDir, symlink); err != nil {
|
2019-11-30 23:17:50 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-12-01 04:02:52 +00:00
|
|
|
val, err := osb.ds.Get(lastSectorIdKey)
|
2019-11-30 23:17:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-12-01 04:02:52 +00:00
|
|
|
if err := sb.ds.Put(lastSectorIdKey, val); err != nil {
|
2019-11-30 23:17:50 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
sb.lastID = osb.lastID
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-12-08 20:06:40 +00:00
|
|
|
func (sb *SectorBuilder) SetLastSectorID(id uint64) error {
|
|
|
|
if err := sb.ds.Put(lastSectorIdKey, []byte(fmt.Sprint(id))); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
sb.lastID = id
|
|
|
|
return nil
|
|
|
|
}
|
2019-12-10 17:11:59 +00:00
|
|
|
|
|
|
|
func migrate(from, to string, symlink bool) error {
|
|
|
|
st, err := os.Stat(from)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if st.IsDir() {
|
|
|
|
return migrateDir(from, to, symlink)
|
|
|
|
}
|
|
|
|
return migrateFile(from, to, symlink)
|
|
|
|
}
|
|
|
|
|
|
|
|
func migrateDir(from, to string, symlink bool) error {
|
|
|
|
tost, err := os.Stat(to)
|
|
|
|
if err != nil {
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := os.MkdirAll(to, 0755); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else if !tost.IsDir() {
|
|
|
|
return xerrors.Errorf("target %q already exists and is a file (expected directory)")
|
|
|
|
}
|
|
|
|
|
|
|
|
dirents, err := ioutil.ReadDir(from)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, inf := range dirents {
|
|
|
|
n := inf.Name()
|
|
|
|
if inf.IsDir() {
|
|
|
|
if err := migrate(filepath.Join(from, n), filepath.Join(to, n), symlink); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if err := migrate(filepath.Join(from, n), filepath.Join(to, n), symlink); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func migrateFile(from, to string, symlink bool) error {
|
|
|
|
if symlink {
|
|
|
|
return os.Symlink(from, to)
|
|
|
|
}
|
|
|
|
|
|
|
|
return dcopy.Copy(from, to)
|
|
|
|
}
|