1 package download
2
3 import (
4 "bytes"
5 "fmt"
6 "io"
7 "math"
8 "net/http"
9 "os"
10 "path"
11 "path/filepath"
12 "strconv"
13 "strings"
14 "sync"
15 "time"
16
17 "github.com/dustin/go-humanize"
18 "github.com/pkg/errors"
19 "gitlab.com/tslocum/gophast/pkg/config"
20 "gitlab.com/tslocum/gophast/pkg/log"
21 . "gitlab.com/tslocum/gophast/pkg/utils"
22 "gitlab.com/tslocum/preallocate"
23 )
24
25 const (
26 WriteBufferSize = 64 * 1024
27 DiskAllocationWarningSize = 24 * 1024 * 1024
28 ProgressCompletedSize = 10000
29 )
30
31 type Download struct {
32 ID int64
33 Status int
34 URL string
35 Name string
36 Size int64
37 Remaining int64
38 SupportsRange bool
39 Ranges []*ByteRange
40 Writers []*DownloadWriter
41 ControlWriters []*ControlWriter
42 FilePath string
43 Started time.Time
44 Finished time.Time
45 WasPaused bool
46
47 file *os.File
48 controlFile *os.File
49 lastWrote time.Time
50 wg *sync.WaitGroup
51 rangeWG *sync.WaitGroup
52 Retries int
53 InProgress bool
54 Paused bool
55 Cancelled bool
56 rangeDownloaded int64
57 resume chan bool
58 err error
59
60 downloading bool
61 printedDownloadError bool
62
63 *sync.RWMutex
64 }
65
66 var (
67 NewID chan int64
68 redirections = make(map[string]string)
69 redirectionsLock = &sync.RWMutex{}
70 )
71
72 func distributeNewDownloadIDs() {
73 var id int64
74 for {
75 id++
76 NewID <- id
77 }
78 }
79
80 func NewDownload(url string, postData []byte) (*Download, error) {
81 if NewID == nil {
82 NewID = make(chan int64)
83 go distributeNewDownloadIDs()
84 }
85
86 log.Standard("Fetching metadata...")
87 metadata, err := FetchMetadata(url, postData)
88 if err != nil {
89 return nil, errors.Wrap(err, "failed to fetch metadata")
90 }
91
92 downloadPath := DownloadPath(metadata.Name)
93
94 waitgroup := new(sync.WaitGroup)
95 waitgroup.Add(1)
96
97 d := &Download{ID: <-NewID, Started: time.Now(), SupportsRange: metadata.SupportsRange, resume: make(chan bool), wg: waitgroup, rangeWG: new(sync.WaitGroup), RWMutex: new(sync.RWMutex)}
98 d.Name = filepath.Base(downloadPath)
99 d.Size = metadata.Size
100 d.URL = url
101 d.FilePath = downloadPath
102
103 return d, nil
104 }
105
106 func (d *Download) GetStatus() int {
107 d.RLock()
108 defer d.RUnlock()
109
110 return d.Status
111 }
112
113 func (d *Download) GetDownloaded() int64 {
114 d.RLock()
115 defer d.RUnlock()
116
117 if d.Size == 0 {
118 return 0
119 } else if d.Status > 0 {
120 return d.Size
121 }
122
123 var downloaded int64
124 if d.Remaining > 0 {
125 downloaded = d.Size - d.Remaining
126 }
127 for _, r := range d.Ranges {
128 downloaded += r.Wrote
129 }
130
131 return downloaded
132 }
133
134 func (d *Download) GetVerboseDownloaded() []byte {
135 d.RLock()
136 defer d.RUnlock()
137
138 if d.Status > 0 {
139 return []byte(strconv.Itoa(int(ProgressCompletedSize)))
140 } else if d.Size == 0 || len(d.Ranges) == 0 {
141 return []byte(fmt.Sprintf("0,%d", ProgressCompletedSize))
142 }
143
144 var out []byte
145 var rangeSize, progressSize, progress, remaining, totalProgress int64
146 lastRange := len(d.Ranges) - 1
147
148 if d.Remaining != d.Size && d.Remaining > 0 {
149 progress = (d.Size - d.Remaining) * ProgressCompletedSize / d.Size
150
151 out = []byte(fmt.Sprintf("%d,%d", progress, 0))
152 totalProgress += progress
153 }
154
155 for i, r := range d.Ranges {
156 rangeSize = r.End - r.Start + 1
157 progressSize = (rangeSize * ProgressCompletedSize / d.Size)
158 progress = (r.Progress * ProgressCompletedSize / d.Size)
159
160 if i == lastRange && r.Progress == rangeSize && totalProgress+progress < ProgressCompletedSize {
161 progress = ProgressCompletedSize - totalProgress
162
163 remaining = 0
164 } else {
165 remaining = (rangeSize - r.Progress) * ProgressCompletedSize / d.Size
166 if i == lastRange && totalProgress+progress+remaining != ProgressCompletedSize {
167 remaining = ProgressCompletedSize - (totalProgress + progress)
168 }
169 }
170
171 if progress+remaining < progressSize {
172 d := progressSize - (progress + remaining)
173 if totalProgress+progress+remaining+d < ProgressCompletedSize {
174 if remaining > 0 {
175 remaining += d
176 } else {
177 progress += d
178 }
179 }
180 }
181
182 if out != nil {
183 out = append(out, byte(','))
184 }
185 out = append(out, []byte(fmt.Sprintf("%d,%d", progress, remaining))...)
186 totalProgress += progress + remaining
187 }
188
189 return out
190 }
191
192 func DownloadPath(defaultFileName string) string {
193 downloadName := config.C.DownloadName
194 if downloadName == "" {
195 downloadName = defaultFileName
196 }
197 if filepath.IsAbs(downloadName) {
198 return downloadName
199 }
200
201 downloadDir, err := filepath.Abs(config.C.DownloadDir)
202 if err != nil {
203 return ""
204 }
205 return filepath.Join(downloadDir, downloadName)
206 }
207
208 func FetchMetadata(url string, postData []byte) (*Metadata, error) {
209 resp, err := request("HEAD", url, postData, 0, 0)
210 if err != nil {
211 return nil, err
212 }
213 defer resp.Body.Close()
214
215 if resp.StatusCode == 404 {
216 return nil, errors.New("file not found (404)")
217 } else if resp.StatusCode != 200 {
218 return nil, errors.New(fmt.Sprintf("server responded with status %d (expected 200 OK)", resp.StatusCode))
219 }
220
221
222 fileName := path.Base(resp.Request.URL.String())
223
224
225 dispositionSplit := strings.Split(resp.Header.Get("Content-Disposition"), ";")
226 for _, ds := range dispositionSplit {
227 ds = strings.TrimSpace(ds)
228
229 if !strings.HasPrefix(ds, "filename=") {
230 continue
231 }
232
233 ds = ds[9:]
234
235 if ds[0] == '"' && ds[len(ds)-1] == '"' && len(ds) > 2 {
236 ds = ds[1 : len(ds)-1]
237 }
238
239 if path.Base(ds) != "" {
240 fileName = path.Base(ds)
241
242 break
243 }
244 }
245
246 supportsRange := resp.ContentLength > 0 && strings.Contains(resp.Header.Get("Accept-Ranges"), "bytes")
247 if !supportsRange {
248 log.Standardf("%s does not support partial downloads", url)
249 }
250
251 return &Metadata{Name: fileName, Size: resp.ContentLength, SupportsRange: supportsRange}, nil
252 }
253
254 func request(method string, url string, postData []byte, rangeStart int64, rangeEnd int64) (*http.Response, error) {
255 client := &http.Client{}
256
257 var (
258 timeout = time.NewTimer(config.C.ConnectTimeout)
259 req *http.Request
260 err error
261 )
262
263 requestComplete := make(chan bool)
264 go func() {
265 req, err = http.NewRequest(method, url, bytes.NewReader(postData))
266 requestComplete <- true
267 }()
268
269 select {
270 case <-requestComplete:
271 if err != nil {
272 return nil, err
273 }
274 case <-timeout.C:
275 return nil, errors.New("connection timed out")
276 }
277
278 if config.C.UserAgent == "" {
279 config.C.SetUserAgent()
280 }
281 req.Header.Add("User-Agent", config.C.UserAgent)
282
283 if rangeEnd > 0 {
284 req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", rangeStart, rangeEnd))
285 }
286
287 resp, err := client.Do(req)
288 if err == nil {
289
290 printRedirectionWarning(url, resp.Request.URL.String())
291 }
292
293 return resp, err
294 }
295
296 func (d *Download) downloadRange(postData []byte, r *ByteRange, id int, maxid int, w *DownloadWriter) {
297 var (
298 done bool
299 wrote int64
300 start = time.Now()
301 rangeSize = r.End - r.Start + 1
302 )
303
304 defer func() {
305 done = true
306 d.rangeWG.Done()
307 }()
308
309 rangeStart := r.Start
310 rangeEnd := r.End
311 expectedStatusCode := http.StatusPartialContent
312 if rangeStart == 0 && rangeEnd == d.Size-1 {
313
314 rangeEnd = 0
315 expectedStatusCode = http.StatusOK
316 }
317
318 resp, err := request("GET", d.URL, postData, rangeStart, rangeEnd)
319 if err != nil {
320 d.Status = -1
321 log.Verbosef("Range %d failed to connect", id+1)
322 d.err = errors.Wrap(err, "file transfer interrupted")
323 return
324 }
325 defer resp.Body.Close()
326
327 if resp.StatusCode != expectedStatusCode {
328 if resp.StatusCode == http.StatusServiceUnavailable {
329 d.Status = -1
330 } else {
331 d.Cancel()
332 }
333 d.err = errors.Errorf("server sent response code: %d (expected: %d)", resp.StatusCode, expectedStatusCode)
334 return
335 }
336
337 go func() {
338 for {
339 d.Lock()
340 if config.C.TransferTimeout == 0 || d.Cancelled || d.Status > 0 || done {
341 d.Unlock()
342 return
343 }
344
345 if d.downloading {
346 if r.LastWrote.After(d.lastWrote) {
347 d.lastWrote = r.LastWrote
348 }
349
350 if time.Now().Sub(d.lastWrote) >= config.C.TransferTimeout {
351 d.Status = -1
352 log.Verbosef("Range %d interrupted", id+1)
353 d.err = errors.Wrap(errors.New("timed out"), "file transfer interrupted")
354 d.Unlock()
355 return
356 }
357 }
358 d.Unlock()
359
360 time.Sleep(config.C.TransferTimeout)
361 }
362 }()
363
364 wrote, err = io.CopyBuffer(w, resp.Body, make([]byte, WriteBufferSize))
365 if err != nil {
366 if !d.Paused && !d.Cancelled && err.Error() != "cancelled" && !strings.Contains(err.Error(), "failed to write to disk") {
367 d.Lock()
368 log.Verbosef("Range %d interrupted", id+1)
369 d.Status = -1
370 d.err = errors.Wrap(err, "file transfer interrupted")
371 d.Unlock()
372 }
373
374 return
375 }
376 if wrote != rangeSize {
377 log.Verbosef("Range %d interrupted", id+1)
378 d.Lock()
379 d.Status = -1
380 d.err = errors.New("file transfer interrupted")
381 d.Unlock()
382 return
383 }
384
385 log.Verbosef("Finished range %d/%d (%s in %s at %s/s)", id+1, maxid, humanize.Bytes(uint64(rangeSize)), FormatDuration(time.Since(start)), humanize.Bytes(uint64(float64(float64(rangeSize))/time.Since(start).Seconds())))
386
387 d.Lock()
388 d.rangeDownloaded += rangeSize
389 d.Unlock()
390 }
391
392 func (d *Download) Download(postData []byte) error {
393 defer d.wg.Done()
394
395 d.Lock()
396 if d.InProgress {
397 d.Unlock()
398 return errors.New("download already in progress")
399 } else if d.Cancelled {
400 d.Unlock()
401 return errors.New("download cancelled")
402 }
403 d.lastWrote = time.Now()
404 d.InProgress = true
405 d.downloading = true
406 d.Unlock()
407
408 var (
409 resumeDownload bool
410 fileName = filepath.Base(d.FilePath)
411 existingSize int64
412 control *ControlFile
413 err error
414 )
415 d.Started = time.Now()
416
417 if config.C.Resume {
418 if !d.WasPaused {
419 _, err := os.Stat(d.FilePath + ".gophast")
420 if err == nil || os.IsExist(err) {
421 resumeDownload = true
422
423 control, err = ParseControlFile(d.FilePath + ".gophast")
424 if err != nil {
425 d.Status = -1
426 return errors.Wrap(err, "failed to read control file")
427 } else if control != nil && len(control.URLs) > 0 && len(control.Ranges) > 0 {
428 resumeDownload = true
429
430 d.Ranges = control.Ranges
431 d.resetRanges()
432 }
433 }
434
435 if ex, err := os.Stat(d.FilePath); err == nil {
436 existingSize = ex.Size()
437
438 if !config.C.Force {
439 if !resumeDownload && existingSize == d.Size {
440 d.Status = 2
441 return errors.New("already downloaded (use --force to download anyway)")
442 } else if resumeDownload && existingSize > 0 && existingSize != d.Size {
443 d.Cancel()
444 return errors.New("failed to resume: existing file does not match expected size (use --force to download anyway)")
445 }
446 }
447 } else if !os.IsNotExist(err) {
448 d.Cancel()
449 return errors.Errorf("failed to check file status: %v", err)
450 }
451 } else {
452 resumeDownload = true
453 d.Paused = false
454
455 d.Writers = nil
456 d.ControlWriters = nil
457 d.Remaining = 0
458 d.rangeDownloaded = 0
459 existingSize = d.Size
460
461 d.resetRanges()
462 }
463 }
464
465 d.file, err = os.OpenFile(d.FilePath, os.O_CREATE|os.O_WRONLY, 0644)
466 if err != nil {
467 d.Cancel()
468 return errors.Wrap(err, "failed to open/create file")
469 }
470
471 if existingSize != d.Size {
472 if existingSize < d.Size && !resumeDownload {
473 if d.Size >= DiskAllocationWarningSize {
474 log.Standard("Allocating disk space...")
475 }
476 err = preallocate.File(d.file, d.Size)
477 } else if existingSize > d.Size {
478 err = d.file.Truncate(d.Size)
479 }
480 if err != nil {
481 _ = d.file.Close()
482 d.Cancel()
483 return errors.Wrap(err, "failed to resize file")
484 }
485 }
486
487 if d.Size == 0 {
488 log.Standardf("%s downloaded", d.Name)
489
490 _ = d.file.Close()
491 d.Status = 1
492 return nil
493 }
494
495 if d.SupportsRange {
496 if len(d.Ranges) == 0 {
497 d.Remaining = d.Size
498
499 pieceCount, pieceSize := d.pieceCountAndSize()
500
501 var (
502 rangeStart, rangeEnd int64
503 r *ByteRange
504 )
505 for i := int64(1); i <= pieceCount; i++ {
506 if i < pieceCount {
507 rangeEnd += pieceSize
508 } else {
509 rangeEnd = d.Size - 1
510 }
511
512 r = NewByteRange(rangeStart, rangeEnd, 0)
513
514 d.Ranges = append(d.Ranges, r)
515
516 rangeStart = rangeEnd + 1
517 }
518 } else {
519 for _, r := range d.Ranges {
520 d.Remaining += r.End - (r.Start + r.Wrote) + 1
521 }
522 }
523 } else {
524 d.Remaining = d.Size
525
526 r := NewByteRange(0, d.Size-1, 0)
527 d.Ranges = []*ByteRange{r}
528 }
529 pieceCount := len(d.Ranges)
530
531 if d.Remaining == 0 && !config.C.Force {
532 _ = d.file.Close()
533 d.Status = 2
534 return errors.New("already downloaded (use --force to download anyway)")
535 }
536
537 logMessageLabel := "Downloading"
538 logMessageSize := humanize.Bytes(uint64(d.Size))
539 if resumeDownload && d.Remaining > 0 && d.Remaining < d.Size {
540 logMessageLabel = "Resuming"
541 logMessageSize = fmt.Sprintf("%.0f%% - %s / %s", math.Floor(float64(d.Size-d.Remaining)/float64(d.Size)*100), humanize.Bytes(uint64(d.Size-d.Remaining)), logMessageSize)
542 }
543 log.Standardf("%s %s... (%s)", logMessageLabel, d.Name, logMessageSize)
544
545 if config.C.ProgressLevel == config.ProgressDynamic && pieceCount > 1 {
546 log.ForcePrint("")
547 }
548
549 var (
550 controlWrote int
551 controlCursor int
552
553 cw *ControlWriter
554 )
555 if config.C.Resume {
556 d.controlFile, err = os.OpenFile(d.FilePath+".gophast", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
557 if err != nil {
558 d.Status = -1
559 return errors.Errorf("failed to open/create control file: %v (use --no-resume to download anyway)", err)
560 }
561
562 controlWrote, _ = d.controlFile.WriteString(d.URL + "\n")
563 controlCursor += controlWrote
564 }
565
566 config.C.TaskFormat = "Range %" + strconv.Itoa(len(fmt.Sprintf("%d", pieceCount))) + "d"
567 config.C.TaskWidth = int(math.Max(float64(len(Ellipsize(fileName))), float64(len(fmt.Sprintf(config.C.TaskFormat, pieceCount)))))
568
569 var (
570 controlLine string
571 controlLineMaxSizeLen int
572 )
573 for i, r := range d.Ranges {
574 if r.Wrote == r.End-r.Start+1 {
575 continue
576 }
577
578 log.Verbosef("Starting range %d/%d %s", i+1, pieceCount, r)
579
580 if d.Retries == 0 {
581 AddLocalProgressBar(i, pieceCount, r)
582 }
583
584 controlLineMaxSizeLen = len(fmt.Sprintf("%d", r.End-r.Start))
585
586 if config.C.Resume {
587 controlLine = fmt.Sprintf("%d,%d,%0."+strconv.Itoa(controlLineMaxSizeLen)+"d\n", r.Start, r.End, 0)
588
589 cw = NewControlWriter(d.controlFile, int64(controlCursor+strings.LastIndex(controlLine, ",")+1))
590 d.ControlWriters = append(d.ControlWriters, cw)
591
592 controlWrote, _ = d.controlFile.WriteString(controlLine)
593 controlCursor += controlWrote
594 }
595
596 go d.handleWriteControlFile(cw, r, controlLineMaxSizeLen)
597
598 w := NewDownloadWriter(d.file, r)
599 d.Writers = append(d.Writers, w)
600
601 d.rangeWG.Add(1)
602 go d.downloadRange(postData, r, i, pieceCount, w)
603 }
604
605 if d.Retries == 0 {
606 AddGlobalProgressBar(d)
607 }
608
609
610 d.rangeWG.Wait()
611
612 d.Lock()
613
614 d.downloading = false
615 if d.Remaining > 0 && d.rangeDownloaded == d.Remaining {
616 d.Status = 1
617 }
618
619 var retry bool
620 if d.Status <= 0 && !d.Cancelled && (d.err == nil || strings.Contains(d.err.Error(), "file transfer interrupted")) && (d.Paused || config.C.MaxRetries == 0 || d.Retries < config.C.MaxRetries) {
621 retry = true
622 }
623
624 d.Unlock()
625
626 _ = d.file.Close()
627
628 if config.C.Resume {
629 if d.GetStatus() <= 0 {
630 d.cancelWriters()
631 _ = d.controlFile.Close()
632 } else {
633 _ = d.controlFile.Close()
634 _ = os.Remove(d.FilePath + ".gophast")
635 }
636 }
637
638 if retry {
639 d.Lock()
640 d.Retries++
641 d.Unlock()
642
643 if !d.Paused {
644 retryDelay := ""
645 if config.C.RetryDelay > 0 {
646 retryDelay = fmt.Sprintf(" in %s", config.C.RetryDelay)
647 }
648 log.Verbosef("Download interrupted, retrying%s...", retryDelay)
649 time.Sleep(config.C.RetryDelay)
650 } else {
651 <-d.resume
652 }
653
654 d.Lock()
655 if !d.Cancelled {
656 d.InProgress = false
657 d.Unlock()
658 return d.Download(postData)
659 } else {
660 d.Unlock()
661 return nil
662 }
663 } else if d.GetStatus() > 0 {
664 d.Finished = time.Now()
665 log.Verbosef("%s downloaded (%s in %s at %s/s)", Ellipsize(d.Name), humanize.Bytes(uint64(d.Remaining)), FormatDuration(time.Since(d.Started)), humanize.Bytes(uint64(float64(float64(d.Remaining))/time.Since(d.Started).Seconds())))
666
667 return nil
668 } else {
669 d.Cancel()
670 return d.err
671 }
672 }
673
674 func (d *Download) handleWriteControlFile(cw *ControlWriter, r *ByteRange, maxsizelen int) {
675 if cw == nil {
676 return
677 }
678
679 var autoSave <-chan time.Time
680 if config.C.AutoSaveControl > 0 {
681 ticker := time.NewTicker(config.C.AutoSaveControl)
682 autoSave = ticker.C
683 }
684
685 var (
686 format = "%0." + strconv.Itoa(maxsizelen) + "d"
687 pbytes []byte
688 lastProgress int64
689 cancelled bool
690 rangeSize = r.End - r.Start + 1
691 )
692 for {
693 select {
694 case <-cw.Cancelled:
695 cancelled = true
696 case <-autoSave:
697 }
698
699 if r.Wrote != lastProgress {
700 pbytes = []byte(fmt.Sprintf(format, r.Wrote))
701 ReverseBytes(pbytes)
702
703 _, err := cw.Write(pbytes)
704 if err != nil || d.GetStatus() > 0 || r.Wrote == rangeSize {
705 return
706 }
707
708 lastProgress = r.Wrote
709 }
710
711 if cancelled {
712 return
713 }
714 }
715 }
716
717 func (d *Download) Pause() {
718 d.Lock()
719
720 if d.Paused {
721 d.Unlock()
722 return
723 }
724
725 d.Paused = true
726 d.WasPaused = true
727
728 d.Unlock()
729
730 log.Verbosef("Paused %s", d.Name)
731
732 d.cancelWriters()
733 }
734
735 func (d *Download) Resume() {
736 d.Lock()
737 defer d.Unlock()
738 if !d.Paused {
739 return
740 }
741
742 log.Verbosef("Resuming %s", d.Name)
743
744 d.Paused = false
745 go func(d *Download) {
746 d.resume <- true
747 }(d)
748 }
749
750 func (d *Download) Cancel() {
751 d.Lock()
752
753 if d.Cancelled || d.Status > 0 {
754 d.Unlock()
755 return
756 }
757
758 d.Status = -7
759 d.Cancelled = true
760 if d.Paused {
761 d.Paused = false
762 }
763
764 d.Unlock()
765
766 d.cancelWriters()
767 }
768
769 func (d *Download) cancelWriters() {
770 d.Lock()
771 defer d.Unlock()
772
773 for _, w := range d.Writers {
774 w.Cancelled = true
775 }
776 d.Writers = nil
777
778 var cancelledAll bool
779 for {
780 cancelledAll = true
781
782 for _, cw := range d.ControlWriters {
783 select {
784 case cw.Cancelled <- true:
785 cancelledAll = false
786 default:
787 }
788 }
789
790 if cancelledAll {
791 d.ControlWriters = nil
792
793 return
794 }
795
796 time.Sleep(5 * time.Millisecond)
797 }
798 }
799
800 func (d *Download) resetRanges() {
801 for _, r := range d.Ranges {
802 if r.Wrote > 0 {
803 r.Start += r.Wrote
804
805 r.Wrote = 0
806 }
807 }
808 }
809
810 func (d *Download) remainingRanges() int {
811 var remainingRanges int
812
813 for _, r := range d.Ranges {
814 if r.Wrote < (r.End-r.Start)+1 {
815 remainingRanges++
816 }
817 }
818
819 return remainingRanges
820 }
821
822 func (d *Download) Wait() {
823 if d.wg == nil {
824 return
825 }
826
827 d.wg.Wait()
828 }
829
830 func (d *Download) pieceCountAndSize() (int64, int64) {
831 pieceCount := int64(1)
832
833 if config.C.MinSplitSize == 0 {
834 if config.C.MaxConnections > 0 {
835 pieceCount = config.C.MaxConnections
836 }
837 } else if d.Size >= (config.C.MinSplitSize * 2) {
838 pieceCount = int64(d.Size / config.C.MinSplitSize)
839 if pieceCount > config.C.MaxConnections {
840 pieceCount = config.C.MaxConnections
841 }
842 }
843 if pieceCount > d.Size {
844 pieceCount = d.Size
845 }
846
847 return pieceCount, int64(d.Size / pieceCount)
848 }
849
850 func printRedirectionWarning(original, final string) {
851 if original == final {
852 return
853 }
854
855 redirectionsLock.RLock()
856 if destination, ok := redirections[original]; ok {
857 if destination == final {
858 redirectionsLock.RUnlock()
859 return
860 }
861 }
862 redirectionsLock.RUnlock()
863
864 redirectionsLock.Lock()
865 redirections[original] = final
866 log.Verbosef("Redirected from %s to %s", original, final)
867 redirectionsLock.Unlock()
868 }
869
View as plain text