-
Notifications
You must be signed in to change notification settings - Fork 14
/
main.go
805 lines (719 loc) · 19.4 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
const (
KeySpace = "ipfs-sync."
API = "/api/v0/"
)
func findInStringSlice(slice []string, val string) int {
for i, item := range slice {
if item == val {
return i
}
}
return -1
}
// doRequest does an API request to the node specified in EndPoint. If timeout is 0 it isn't used.
func doRequest(timeout time.Duration, cmd string) (string, error) {
var cancel context.CancelFunc
ctx := context.Background()
if timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
c := &http.Client{}
req, err := http.NewRequestWithContext(ctx, "POST", EndPoint+API+cmd, nil)
if err != nil {
return "", err
}
resp, err := c.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
errStruct := new(ErrorStruct)
err = json.Unmarshal(body, errStruct)
if err == nil {
if errStruct.Error() != "" {
return string(body), errStruct
}
}
return string(body), nil
}
// HashStruct is useful when you only care about the returned hash.
type HashStruct struct {
Hash string
}
// GetFileCID gets a file CID based on MFS path relative to BasePath.
func GetFileCID(filePath string) string {
out, _ := doRequest(TimeoutTime, "files/stat?hash=true&arg="+url.QueryEscape(BasePath+filePath))
fStat := new(HashStruct)
err := json.Unmarshal([]byte(out), &fStat)
if err != nil {
return ""
}
return fStat.Hash
}
// RemoveFile removes a file from the MFS relative to BasePath.
func RemoveFile(fpath string) error {
_, err := doRequest(TimeoutTime, fmt.Sprintf(`files/rm?arg=%s&force=true`, url.QueryEscape(BasePath+fpath)))
return err
}
// MakeDir makes a directory along with parents in path
func MakeDir(path string) error {
_, err := doRequest(TimeoutTime, fmt.Sprintf(`files/mkdir?arg=%s&parents=true`, url.QueryEscape(BasePath+path)))
return err
}
func filePathWalkDir(root string) ([]string, error) {
var files []string
err := filepath.WalkDir(root, func(path string, info fs.DirEntry, err error) error {
if info == nil {
return errors.New(fmt.Sprintf("cannot access '%s' for crawling", path))
}
if !info.IsDir() {
filePathSplit := strings.Split(path, string(os.PathSeparator))
if IgnoreHidden && filePathSplit[len(filePathSplit)-1][0] == '.' {
return nil
}
files = append(files, path)
} else {
dirPathSplit := strings.Split(path, string(os.PathSeparator))
if IgnoreHidden && len(dirPathSplit[len(dirPathSplit)-1]) > 0 && dirPathSplit[len(dirPathSplit)-1][0] == '.' {
return filepath.SkipDir
}
}
return nil
})
return files, err
}
// AddDir adds a directory, and returns CID.
func AddDir(path string, nocopy bool, pin bool, estuary bool) (string, error) {
pathSplit := strings.Split(path, string(os.PathSeparator))
dirName := pathSplit[len(pathSplit)-2]
files, err := filePathWalkDir(path)
if err != nil {
return "", err
}
localDirs := make(map[string]bool)
for _, file := range files {
filePathSplit := strings.Split(file, string(os.PathSeparator))
if IgnoreHidden && filePathSplit[len(filePathSplit)-1][0] == '.' {
continue
}
splitName := strings.Split(file, ".")
if findInStringSlice(Ignore, splitName[len(splitName)-1]) > -1 {
continue
}
parentDir := strings.Join(filePathSplit[:len(filePathSplit)-1], string(os.PathSeparator))
makeDir := !localDirs[parentDir]
if makeDir {
localDirs[parentDir] = true
}
mfsPath := file[len(path):]
if os.PathSeparator != '/' {
mfsPath = strings.ReplaceAll(mfsPath, string(os.PathSeparator), "/")
}
_, err := AddFile(file, dirName+"/"+mfsPath, nocopy, makeDir, false)
if err != nil {
log.Println("Error adding file:", err)
}
}
cid := GetFileCID(dirName)
if pin {
err := Pin(cid)
log.Println("Error pinning", dirName, ":", err)
}
if estuary {
if err := PinEstuary(cid, dirName); err != nil {
log.Println("Error pinning to Estuary:", err)
}
}
return cid, err
}
// A simple IPFS add, if onlyhash is true, only the CID is generated and returned
func IPFSAddFile(fpath string, nocopy, onlyhash bool) (*HashStruct, error) {
client := http.Client{}
f, err := os.Open(fpath)
if err != nil {
return nil, err
}
pr, pw := io.Pipe()
writer := multipart.NewWriter(pw)
defer pr.Close()
req, err := http.NewRequest("POST", EndPoint+API+fmt.Sprintf(`add?nocopy=%t&pin=false&quieter=true&only-hash=%t`, nocopy, onlyhash), pr)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", writer.FormDataContentType())
go func() {
defer f.Close()
defer writer.Close()
h := make(textproto.MIMEHeader)
h.Set("Abspath", fpath)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "file", url.QueryEscape(f.Name())))
h.Set("Content-Type", "application/octet-stream")
part, err := writer.CreatePart(h)
if err != nil {
pw.CloseWithError(err)
return
}
if Verbose {
log.Println("Generating file headers...")
}
_, err = io.Copy(part, f)
pw.CloseWithError(err)
}()
if Verbose {
log.Println("Doing add request...")
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var hash HashStruct
err = json.NewDecoder(resp.Body).Decode(&hash)
if Verbose {
log.Println("File hash:", hash.Hash)
}
return &hash, err
}
// AddFile adds a file to the MFS relative to BasePath. from should be the full path to the file intended to be added.
// If makedir is true, it'll create the directory it'll be placed in.
// If overwrite is true, it'll perform an rm before copying to MFS.
func AddFile(from, to string, nocopy bool, makedir bool, overwrite bool) (string, error) {
log.Println("Adding file from", from, "to", BasePath+to, "...")
hash, err := IPFSAddFile(from, nocopy, false)
if err != nil {
return "", err
}
if makedir {
toSplit := strings.Split(to, "/")
parent := strings.Join(toSplit[:len(toSplit)-1], "/")
if Verbose {
log.Printf("Creating parent directory '%s' in MFS...\n", parent)
}
err = MakeDir(parent)
if err != nil {
return "", err
}
}
if overwrite {
if Verbose {
log.Println("Removing existing file (if any)...")
}
RemoveFile(to)
}
// send files/cp request
if Verbose {
log.Println("Adding file to mfs path:", BasePath+to)
}
_, err = doRequest(TimeoutTime, fmt.Sprintf(`files/cp?arg=%s&arg=%s`, "/ipfs/"+url.QueryEscape(hash.Hash), url.QueryEscape(BasePath+to)))
if err != nil {
if Verbose {
log.Println("Error on files/cp:", err)
log.Println("fpath:", from)
}
if HandleBadBlockError(err, from, nocopy) {
log.Println("files/cp failure due to filestore, retrying (recursive)")
AddFile(from, to, nocopy, makedir, overwrite)
}
}
return hash.Hash, err
}
type FileStoreStatus int
const NoFile FileStoreStatus = 11
type FileStoreKey struct {
Slash string `json:"/"`
}
// FileStoreEntry is for results returned by `filestore/verify`, only processes Status and Key, as that's all ipfs-sync uses.
type FileStoreEntry struct {
Status FileStoreStatus
Key FileStoreKey
}
var fileStoreCleanupLock chan int
func init() {
fileStoreCleanupLock = make(chan int, 1)
}
// FileStoreEntry is for results returned by `filestore/verify`, only processes Status and Key, as that's all ipfs-sync uses.
type RefResp struct {
Err string
Ref string
}
// Completely removes a CID, even if pinned
func RemoveCID(cid string) {
var found bool
// Build our own request because we want to stream data...
c := &http.Client{}
req, err := http.NewRequest("POST", EndPoint+API+"refs?unique=true&recursive=true&arg="+cid, nil)
if err != nil {
log.Println(err)
return
}
// Send request
resp, err := c.Do(req)
if err != nil {
log.Println(err)
return
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
if err != nil {
log.Println(err)
return
}
// Decode the json stream and process it
for dec.More() {
found = true
refResp := new(RefResp)
err := dec.Decode(refResp)
if err != nil {
log.Println("Error decoding ref response stream:", err)
continue
}
newcid := refResp.Ref
if newcid == "" {
newcid = cid
}
if Verbose {
log.Println("Removing block:", newcid)
}
RemoveBlock(newcid)
}
if !found {
if Verbose {
log.Println("Removing block:", cid)
}
RemoveBlock(cid)
}
}
// remove block, even if pinned
func RemoveBlock(cid string) {
var err error
for _, err = doRequest(TimeoutTime, "block/rm?arg="+cid); err != nil && strings.HasPrefix(err.Error(), "pinned"); _, err = doRequest(TimeoutTime, "block/rm?arg="+cid) {
splitErr := strings.Split(err.Error(), " ")
var cid2 string
if len(splitErr) < 3 { // This is caused by IPFS returning "pinned (recursive)", it means the file in question has been explicitly pinned, and for some unknown reason, it chooses to omit the CID in this particular situation
cid2 = cid
} else {
cid2 = splitErr[2]
}
log.Println("Effected block is pinned, removing pin:", cid2)
_, err := doRequest(0, "pin/rm?arg="+cid2) // no timeout
if err != nil {
log.Println("Error removing pin:", err)
}
}
if err != nil {
log.Println("Error removing bad block:", err)
}
}
// CleanFilestore removes blocks that point to files that don't exist
func CleanFilestore() {
select {
case fileStoreCleanupLock <- 1:
defer func() { <-fileStoreCleanupLock }()
default:
return
}
if Verbose {
log.Println("Removing blocks that point to a file that doesn't exist from filestore...")
}
// Build our own request because we want to stream data...
c := &http.Client{}
req, err := http.NewRequest("POST", EndPoint+API+"filestore/verify", nil)
if err != nil {
log.Println(err)
return
}
// Send request
resp, err := c.Do(req)
if err != nil {
log.Println(err)
return
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
if err != nil {
log.Println(err)
return
}
// Decode the json stream and process it
for dec.More() {
fsEntry := new(FileStoreEntry)
err := dec.Decode(fsEntry)
if err != nil {
log.Println("Error decoding fsEntry stream:", err)
continue
}
if fsEntry.Status == NoFile { // if the block points to a file that doesn't exist, remove it.
log.Println("Removing reference from filestore:", fsEntry.Key.Slash)
RemoveBlock(fsEntry.Key.Slash)
}
}
}
// HandleBackBlockError runs CleanFilestore() and returns true if there was a bad block error.
func HandleBadBlockError(err error, fpath string, nocopy bool) bool {
txt := err.Error()
if strings.HasPrefix(txt, "failed to get block") || strings.HasSuffix(txt, "no such file or directory") {
if Verbose {
log.Println("Handling bad block error: " + txt)
}
if fpath == "" { // TODO attempt to get fpath from error msg when possible
CleanFilestore()
} else {
cid, err := IPFSAddFile(fpath, nocopy, true)
if err == nil {
RemoveCID(cid.Hash)
} else {
log.Println("Error handling bad block error:", err)
}
}
return true
}
return false
}
// Pin CID
func Pin(cid string) error {
resp, err := doRequest(0, "pin/add?arg="+url.QueryEscape(cid)) // no timeout
if resp != "" {
if Verbose {
log.Println("Pin response:", resp)
}
}
return err
}
// ErrorStruct allows us to read the errors received by the IPFS daemon.
type ErrorStruct struct {
Message string // used for error text
Error2 string `json:"Error"` // also used for error text
Code int
Type string
}
// Outputs the error text contained in the struct, statistfies error interface.
func (es *ErrorStruct) Error() string {
switch {
case es.Message != "":
return es.Message
case es.Error2 != "":
return es.Error2
}
return ""
}
// UpdatePin updates a recursive pin to a new CID, unpinning old content.
func UpdatePin(from, to string, nocopy bool) {
_, err := doRequest(0, "pin/update?arg="+url.QueryEscape(from)+"&arg="+url.QueryEscape(to)) // no timeout
if err != nil {
log.Println("Error updating pin:", err)
if Verbose {
log.Println("From CID:", from, "To CID:", to)
}
if HandleBadBlockError(err, "", nocopy) {
if Verbose {
log.Println("Bad blocks found, running pin/update again (recursive)")
}
UpdatePin(from, to, nocopy)
return
}
err = Pin(to)
if err != nil {
log.Println("[ERROR] Error adding pin:", err)
}
}
}
// Key contains information about an IPNS key.
type Key struct {
Id string
Name string
}
// Keys is used to store a slice of Key.
type Keys struct {
Keys []Key
}
// ListKeys lists all the keys in the IPFS daemon.
// TODO Only return keys in the namespace.
func ListKeys() (*Keys, error) {
res, err := doRequest(TimeoutTime, "key/list")
if err != nil {
return nil, err
}
keys := new(Keys)
err = json.Unmarshal([]byte(res), keys)
if err != nil {
return nil, err
}
return keys, nil
}
// ResolveIPNS takes an IPNS key and returns the CID it resolves to.
func ResolveIPNS(key string) (string, error) {
res, err := doRequest(0, "name/resolve?arg="+key) // no timeout
if err != nil {
return "", err
}
type PathStruct struct {
Path string
}
path := new(PathStruct)
err = json.Unmarshal([]byte(res), path)
if err != nil {
return "", err
}
pathSplit := strings.Split(path.Path, "/")
if len(pathSplit) < 3 {
return "", errors.New("Unexpected output in name/resolve: " + path.Path)
}
return pathSplit[2], nil
}
// Generates an IPNS key in the keyspace based on name.
func GenerateKey(name string) Key {
res, err := doRequest(TimeoutTime, "key/gen?arg="+KeySpace+name)
if err != nil {
log.Panicln("[ERROR]", err)
}
key := new(Key)
err = json.Unmarshal([]byte(res), key)
if err != nil {
log.Panicln("[ERROR]", err)
}
return *key
}
// Publish CID to IPNS
func Publish(cid, key string) error {
_, err := doRequest(0, fmt.Sprintf("name/publish?arg=%s&key=%s", url.QueryEscape(cid), KeySpace+key)) // no timeout
return err
}
type EstuaryFile struct {
Cid string
Name string
}
type IPFSRemotePinningResponse struct {
Count int
Results []*IPFSRemotePinResult
}
type IPFSRemotePinResult struct {
RequestId string
Pin *IPFSRemotePin
}
type IPFSRemotePin struct {
Cid string
}
func doEstuaryRequest(reqType, cmd string, jsonData []byte) (string, error) {
if EstuaryAPIKey == "" {
return "", errors.New("Estuary API key is blank.")
}
var cancel context.CancelFunc
ctx := context.Background()
if TimeoutTime > 0 {
ctx, cancel = context.WithTimeout(ctx, TimeoutTime)
defer cancel()
}
c := &http.Client{}
var (
req *http.Request
err error
)
if jsonData != nil {
req, err = http.NewRequestWithContext(ctx, reqType, "https://api.estuary.tech/"+cmd, bytes.NewBuffer(jsonData))
} else {
req, err = http.NewRequestWithContext(ctx, reqType, "https://api.estuary.tech/"+cmd, nil)
}
if err != nil {
return "", err
}
req.Header.Add("Authorization", "Bearer "+EstuaryAPIKey)
req.Header.Add("Content-Type", "application/json")
resp, err := c.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
errStruct := new(ErrorStruct)
err = json.Unmarshal(body, errStruct)
if err == nil {
if errStruct.Error() != "" {
return string(body), errStruct
}
}
return string(body), nil
}
func PinEstuary(cid, name string) error {
jsonData, _ := json.Marshal(&EstuaryFile{Cid: cid, Name: name})
_, err := doEstuaryRequest("POST", "pinning/pins", jsonData)
return err
}
func UpdatePinEstuary(oldcid, newcid, name string) {
resp, err := doEstuaryRequest("GET", "pinning/pins?cid="+oldcid, nil)
if err != nil {
log.Println("Error getting Estuary pin:", err)
return
}
pinResp := new(IPFSRemotePinningResponse)
err = json.Unmarshal([]byte(resp), pinResp)
if err != nil {
log.Println("Error decoding Estuary pin list:", err)
return
}
// FIXME Estuary doesn't seem to support `cid` GET field yet, this code can be removed when it does:
var reqId string
pinResp.Count = 0
for _, pinResult := range pinResp.Results {
if pinResult.Pin.Cid == oldcid {
reqId = pinResult.RequestId
pinResp.Count = 1
break
}
}
// END OF FIXME
jsonData, _ := json.Marshal(&EstuaryFile{Cid: newcid, Name: name})
if pinResp.Count > 0 {
_, err := doEstuaryRequest("POST", "pinning/pins/"+reqId, jsonData)
if err != nil {
log.Println("Error updating Estuary pin:", err)
} else {
return
}
}
err = PinEstuary(newcid, name)
if err != nil {
log.Println("Error pinning to Estuary:", err)
}
}
// WatchDog watches for directory updates, periodically updates IPNS records, and updates recursive pins.
func WatchDog() {
// Init WatchDog
keys, err := ListKeys()
if err != nil {
log.Fatalln("Failed to retrieve keys:", err)
}
for _, dk := range DirKeys {
found := false
splitPath := strings.Split(dk.Dir, string(os.PathSeparator))
dk.MFSPath = splitPath[len(splitPath)-2]
// Hash directory if we're using a DB.
if DB != nil {
if Verbose {
log.Println("Hashing", dk.Dir, "...")
}
hashmap, err := HashDir(dk.Dir, dk.DontHash)
if err != nil {
log.Panicln("Error hashing directory for hash DB:", err)
}
localDirs := make(map[string]bool)
HashLock.Lock()
for _, hash := range hashmap {
if hash.Update() {
if Verbose {
log.Println("File updated:", hash.PathOnDisk)
}
// grab parent dir, check if we've already created it
splitName := strings.Split(hash.PathOnDisk, string(os.PathSeparator))
parentDir := strings.Join(splitName[:len(splitName)-1], string(os.PathSeparator))
makeDir := !localDirs[parentDir]
if makeDir {
localDirs[parentDir] = true
}
mfsPath := hash.PathOnDisk[len(dk.Dir):]
if os.PathSeparator != '/' {
mfsPath = strings.ReplaceAll(mfsPath, string(os.PathSeparator), "/")
}
_, err := AddFile(hash.PathOnDisk, dk.MFSPath+"/"+mfsPath, dk.Nocopy, makeDir, false)
if err != nil {
log.Println("Error adding file:", err)
}
}
Hashes[hash.PathOnDisk] = hash
}
HashLock.Unlock()
}
// Check if we recognize any keys, mark them as found, and load them if so.
for _, ik := range keys.Keys {
if ik.Name == KeySpace+dk.ID {
var err error
dk.CID, err = ResolveIPNS(ik.Id)
if err != nil {
log.Println("Error resolving IPNS:", err)
log.Println("Republishing key...")
dk.CID = GetFileCID(dk.MFSPath)
Publish(dk.CID, dk.ID)
}
found = true
log.Println(dk.ID, "loaded:", ik.Id)
watchDir(dk.Dir, dk.Nocopy, dk.DontHash)
break
}
}
if found {
continue
}
log.Println(dk.ID, "not found, generating...")
ik := GenerateKey(dk.ID)
var err error
dk.CID, err = AddDir(dk.Dir, dk.Nocopy, dk.Pin, dk.Estuary)
if err != nil {
log.Panicln("[ERROR] Failed to add directory:", err)
}
Publish(dk.CID, dk.ID)
log.Println(dk.ID, "loaded:", ik.Id)
watchDir(dk.Dir, dk.Nocopy, dk.DontHash)
}
// Main loop
for {
time.Sleep(SyncTime)
for _, dk := range DirKeys {
if fCID := GetFileCID(dk.MFSPath); len(fCID) > 0 && fCID != dk.CID {
// log.Printf("[DEBUG] '%s' != '%s'", fCID, dk.CID)
if dk.Pin {
UpdatePin(dk.CID, fCID, dk.Nocopy)
}
if dk.Estuary {
UpdatePinEstuary(dk.CID, fCID, strings.Split(dk.MFSPath, "/")[0])
}
Publish(fCID, dk.ID)
dk.CID = fCID
log.Println(dk.MFSPath, "updated...")
}
}
}
}
func main() {
// Process config and flags.
ProcessFlags()
log.Println("Starting up ipfs-sync", version, "...")
for _, dk := range DirKeys {
if dk.Nocopy {
// Cleanup filestore first.
if VerifyFilestore {
CleanFilestore()
}
break
}
}
// Start WatchDog.
log.Println("Starting watchdog...")
WatchDog()
}