1
2
3
4
5
6
7 package http
8
9 import (
10 "errors"
11 "fmt"
12 "internal/godebug"
13 "io"
14 "io/fs"
15 "mime"
16 "mime/multipart"
17 "net/http/internal"
18 "net/textproto"
19 "net/url"
20 "os"
21 "path"
22 "path/filepath"
23 "sort"
24 "strconv"
25 "strings"
26 "time"
27 )
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 type Dir string
46
47
48
49
50 func mapOpenError(originalErr error, name string, sep rune, stat func(string) (fs.FileInfo, error)) error {
51 if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) {
52 return originalErr
53 }
54
55 parts := strings.Split(name, string(sep))
56 for i := range parts {
57 if parts[i] == "" {
58 continue
59 }
60 fi, err := stat(strings.Join(parts[:i+1], string(sep)))
61 if err != nil {
62 return originalErr
63 }
64 if !fi.IsDir() {
65 return fs.ErrNotExist
66 }
67 }
68 return originalErr
69 }
70
71
72
73
74 var errInvalidUnsafePath = errors.New("http: invalid or unsafe file path")
75
76
77
78 func (d Dir) Open(name string) (File, error) {
79 path := path.Clean("/" + name)[1:]
80 if path == "" {
81 path = "."
82 }
83 path, err := filepath.Localize(path)
84 if err != nil {
85 return nil, errInvalidUnsafePath
86 }
87 dir := string(d)
88 if dir == "" {
89 dir = "."
90 }
91 fullName := filepath.Join(dir, path)
92 f, err := os.Open(fullName)
93 if err != nil {
94 return nil, mapOpenError(err, fullName, filepath.Separator, os.Stat)
95 }
96 return f, nil
97 }
98
99
100
101
102
103
104
105
106 type FileSystem interface {
107 Open(name string) (File, error)
108 }
109
110
111
112
113
114 type File interface {
115 io.Closer
116 io.Reader
117 io.Seeker
118 Readdir(count int) ([]fs.FileInfo, error)
119 Stat() (fs.FileInfo, error)
120 }
121
122 type anyDirs interface {
123 len() int
124 name(i int) string
125 isDir(i int) bool
126 }
127
128 type fileInfoDirs []fs.FileInfo
129
130 func (d fileInfoDirs) len() int { return len(d) }
131 func (d fileInfoDirs) isDir(i int) bool { return d[i].IsDir() }
132 func (d fileInfoDirs) name(i int) string { return d[i].Name() }
133
134 type dirEntryDirs []fs.DirEntry
135
136 func (d dirEntryDirs) len() int { return len(d) }
137 func (d dirEntryDirs) isDir(i int) bool { return d[i].IsDir() }
138 func (d dirEntryDirs) name(i int) string { return d[i].Name() }
139
140 func dirList(w ResponseWriter, r *Request, f File) {
141
142
143
144 var dirs anyDirs
145 var err error
146 if d, ok := f.(fs.ReadDirFile); ok {
147 var list dirEntryDirs
148 list, err = d.ReadDir(-1)
149 dirs = list
150 } else {
151 var list fileInfoDirs
152 list, err = f.Readdir(-1)
153 dirs = list
154 }
155
156 if err != nil {
157 logf(r, "http: error reading directory: %v", err)
158 Error(w, "Error reading directory", StatusInternalServerError)
159 return
160 }
161 sort.Slice(dirs, func(i, j int) bool { return dirs.name(i) < dirs.name(j) })
162
163 w.Header().Set("Content-Type", "text/html; charset=utf-8")
164 fmt.Fprintf(w, "<!doctype html>\n")
165 fmt.Fprintf(w, "<meta name=\"viewport\" content=\"width=device-width\">\n")
166 fmt.Fprintf(w, "<pre>\n")
167 for i, n := 0, dirs.len(); i < n; i++ {
168 name := dirs.name(i)
169 if dirs.isDir(i) {
170 name += "/"
171 }
172
173
174
175 url := url.URL{Path: name}
176 fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", url.String(), htmlReplacer.Replace(name))
177 }
178 fmt.Fprintf(w, "</pre>\n")
179 }
180
181
182
183 var httpservecontentkeepheaders = godebug.New("httpservecontentkeepheaders")
184
185
186
187
188
189 func serveError(w ResponseWriter, text string, code int) {
190 h := w.Header()
191
192 nonDefault := false
193 for _, k := range []string{
194 "Cache-Control",
195 "Content-Encoding",
196 "Etag",
197 "Last-Modified",
198 } {
199 if !h.has(k) {
200 continue
201 }
202 if httpservecontentkeepheaders.Value() == "1" {
203 nonDefault = true
204 } else {
205 h.Del(k)
206 }
207 }
208 if nonDefault {
209 httpservecontentkeepheaders.IncNonDefault()
210 }
211
212 Error(w, text, code)
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 func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
247 sizeFunc := func() (int64, error) {
248 size, err := content.Seek(0, io.SeekEnd)
249 if err != nil {
250 return 0, errSeeker
251 }
252 _, err = content.Seek(0, io.SeekStart)
253 if err != nil {
254 return 0, errSeeker
255 }
256 return size, nil
257 }
258 serveContent(w, req, name, modtime, sizeFunc, content)
259 }
260
261
262
263
264
265 var errSeeker = errors.New("seeker can't seek")
266
267
268
269 var errNoOverlap = errors.New("invalid range: failed to overlap")
270
271
272
273
274
275 func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) {
276 setLastModified(w, modtime)
277 done, rangeReq := checkPreconditions(w, r, modtime)
278 if done {
279 return
280 }
281
282 code := StatusOK
283
284
285
286 ctypes, haveType := w.Header()["Content-Type"]
287 var ctype string
288 if !haveType {
289 ctype = mime.TypeByExtension(filepath.Ext(name))
290 if ctype == "" {
291
292 var buf [internal.SniffLen]byte
293 n, _ := io.ReadFull(content, buf[:])
294 ctype = DetectContentType(buf[:n])
295 _, err := content.Seek(0, io.SeekStart)
296 if err != nil {
297 serveError(w, "seeker can't seek", StatusInternalServerError)
298 return
299 }
300 }
301 w.Header().Set("Content-Type", ctype)
302 } else if len(ctypes) > 0 {
303 ctype = ctypes[0]
304 }
305
306 size, err := sizeFunc()
307 if err != nil {
308 serveError(w, err.Error(), StatusInternalServerError)
309 return
310 }
311 if size < 0 {
312
313 serveError(w, "negative content size computed", StatusInternalServerError)
314 return
315 }
316
317
318 sendSize := size
319 var sendContent io.Reader = content
320 ranges, err := parseRange(rangeReq, size)
321 switch err {
322 case nil:
323 case errNoOverlap:
324 if size == 0 {
325
326
327
328
329 ranges = nil
330 break
331 }
332 w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", size))
333 fallthrough
334 default:
335 serveError(w, err.Error(), StatusRequestedRangeNotSatisfiable)
336 return
337 }
338
339 if sumRangesSize(ranges) > size {
340
341
342
343
344 ranges = nil
345 }
346 switch {
347 case len(ranges) == 1:
348
349
350
351
352
353
354
355
356
357
358
359 ra := ranges[0]
360 if _, err := content.Seek(ra.start, io.SeekStart); err != nil {
361 serveError(w, err.Error(), StatusRequestedRangeNotSatisfiable)
362 return
363 }
364 sendSize = ra.length
365 code = StatusPartialContent
366 w.Header().Set("Content-Range", ra.contentRange(size))
367 case len(ranges) > 1:
368 sendSize = rangesMIMESize(ranges, ctype, size)
369 code = StatusPartialContent
370
371 pr, pw := io.Pipe()
372 mw := multipart.NewWriter(pw)
373 w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary())
374 sendContent = pr
375 defer pr.Close()
376 go func() {
377 for _, ra := range ranges {
378 part, err := mw.CreatePart(ra.mimeHeader(ctype, size))
379 if err != nil {
380 pw.CloseWithError(err)
381 return
382 }
383 if _, err := content.Seek(ra.start, io.SeekStart); err != nil {
384 pw.CloseWithError(err)
385 return
386 }
387 if _, err := io.CopyN(part, content, ra.length); err != nil {
388 pw.CloseWithError(err)
389 return
390 }
391 }
392 mw.Close()
393 pw.Close()
394 }()
395 }
396
397 w.Header().Set("Accept-Ranges", "bytes")
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 if len(ranges) > 0 || w.Header().Get("Content-Encoding") == "" {
425 w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
426 }
427 w.WriteHeader(code)
428
429 if r.Method != "HEAD" {
430 io.CopyN(w, sendContent, sendSize)
431 }
432 }
433
434
435
436
437 func scanETag(s string) (etag string, remain string) {
438 s = textproto.TrimString(s)
439 start := 0
440 if strings.HasPrefix(s, "W/") {
441 start = 2
442 }
443 if len(s[start:]) < 2 || s[start] != '"' {
444 return "", ""
445 }
446
447
448 for i := start + 1; i < len(s); i++ {
449 c := s[i]
450 switch {
451
452 case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
453 case c == '"':
454 return s[:i+1], s[i+1:]
455 default:
456 return "", ""
457 }
458 }
459 return "", ""
460 }
461
462
463
464 func etagStrongMatch(a, b string) bool {
465 return a == b && a != "" && a[0] == '"'
466 }
467
468
469
470 func etagWeakMatch(a, b string) bool {
471 return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/")
472 }
473
474
475
476 type condResult int
477
478 const (
479 condNone condResult = iota
480 condTrue
481 condFalse
482 )
483
484 func checkIfMatch(w ResponseWriter, r *Request) condResult {
485 im := r.Header.Get("If-Match")
486 if im == "" {
487 return condNone
488 }
489 for {
490 im = textproto.TrimString(im)
491 if len(im) == 0 {
492 break
493 }
494 if im[0] == ',' {
495 im = im[1:]
496 continue
497 }
498 if im[0] == '*' {
499 return condTrue
500 }
501 etag, remain := scanETag(im)
502 if etag == "" {
503 break
504 }
505 if etagStrongMatch(etag, w.Header().get("Etag")) {
506 return condTrue
507 }
508 im = remain
509 }
510
511 return condFalse
512 }
513
514 func checkIfUnmodifiedSince(r *Request, modtime time.Time) condResult {
515 ius := r.Header.Get("If-Unmodified-Since")
516 if ius == "" || isZeroTime(modtime) {
517 return condNone
518 }
519 t, err := ParseTime(ius)
520 if err != nil {
521 return condNone
522 }
523
524
525
526 modtime = modtime.Truncate(time.Second)
527 if ret := modtime.Compare(t); ret <= 0 {
528 return condTrue
529 }
530 return condFalse
531 }
532
533 func checkIfNoneMatch(w ResponseWriter, r *Request) condResult {
534 inm := r.Header.get("If-None-Match")
535 if inm == "" {
536 return condNone
537 }
538 buf := inm
539 for {
540 buf = textproto.TrimString(buf)
541 if len(buf) == 0 {
542 break
543 }
544 if buf[0] == ',' {
545 buf = buf[1:]
546 continue
547 }
548 if buf[0] == '*' {
549 return condFalse
550 }
551 etag, remain := scanETag(buf)
552 if etag == "" {
553 break
554 }
555 if etagWeakMatch(etag, w.Header().get("Etag")) {
556 return condFalse
557 }
558 buf = remain
559 }
560 return condTrue
561 }
562
563 func checkIfModifiedSince(r *Request, modtime time.Time) condResult {
564 if r.Method != "GET" && r.Method != "HEAD" {
565 return condNone
566 }
567 ims := r.Header.Get("If-Modified-Since")
568 if ims == "" || isZeroTime(modtime) {
569 return condNone
570 }
571 t, err := ParseTime(ims)
572 if err != nil {
573 return condNone
574 }
575
576
577 modtime = modtime.Truncate(time.Second)
578 if ret := modtime.Compare(t); ret <= 0 {
579 return condFalse
580 }
581 return condTrue
582 }
583
584 func checkIfRange(w ResponseWriter, r *Request, modtime time.Time) condResult {
585 if r.Method != "GET" && r.Method != "HEAD" {
586 return condNone
587 }
588 ir := r.Header.get("If-Range")
589 if ir == "" {
590 return condNone
591 }
592 etag, _ := scanETag(ir)
593 if etag != "" {
594 if etagStrongMatch(etag, w.Header().Get("Etag")) {
595 return condTrue
596 } else {
597 return condFalse
598 }
599 }
600
601
602 if modtime.IsZero() {
603 return condFalse
604 }
605 t, err := ParseTime(ir)
606 if err != nil {
607 return condFalse
608 }
609 if t.Unix() == modtime.Unix() {
610 return condTrue
611 }
612 return condFalse
613 }
614
615 var unixEpochTime = time.Unix(0, 0)
616
617
618 func isZeroTime(t time.Time) bool {
619 return t.IsZero() || t.Equal(unixEpochTime)
620 }
621
622 func setLastModified(w ResponseWriter, modtime time.Time) {
623 if !isZeroTime(modtime) {
624 w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat))
625 }
626 }
627
628 func writeNotModified(w ResponseWriter) {
629
630
631
632
633
634 h := w.Header()
635 delete(h, "Content-Type")
636 delete(h, "Content-Length")
637 delete(h, "Content-Encoding")
638 if h.Get("Etag") != "" {
639 delete(h, "Last-Modified")
640 }
641 w.WriteHeader(StatusNotModified)
642 }
643
644
645
646 func checkPreconditions(w ResponseWriter, r *Request, modtime time.Time) (done bool, rangeHeader string) {
647
648 ch := checkIfMatch(w, r)
649 if ch == condNone {
650 ch = checkIfUnmodifiedSince(r, modtime)
651 }
652 if ch == condFalse {
653 w.WriteHeader(StatusPreconditionFailed)
654 return true, ""
655 }
656 switch checkIfNoneMatch(w, r) {
657 case condFalse:
658 if r.Method == "GET" || r.Method == "HEAD" {
659 writeNotModified(w)
660 return true, ""
661 } else {
662 w.WriteHeader(StatusPreconditionFailed)
663 return true, ""
664 }
665 case condNone:
666 if checkIfModifiedSince(r, modtime) == condFalse {
667 writeNotModified(w)
668 return true, ""
669 }
670 }
671
672 rangeHeader = r.Header.get("Range")
673 if rangeHeader != "" && checkIfRange(w, r, modtime) == condFalse {
674 rangeHeader = ""
675 }
676 return false, rangeHeader
677 }
678
679
680 func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
681 const indexPage = "/index.html"
682
683
684
685
686 if strings.HasSuffix(r.URL.Path, indexPage) {
687 localRedirect(w, r, "./")
688 return
689 }
690
691 f, err := fs.Open(name)
692 if err != nil {
693 msg, code := toHTTPError(err)
694 serveError(w, msg, code)
695 return
696 }
697 defer f.Close()
698
699 d, err := f.Stat()
700 if err != nil {
701 msg, code := toHTTPError(err)
702 serveError(w, msg, code)
703 return
704 }
705
706 if redirect {
707
708
709 url := r.URL.Path
710 if d.IsDir() {
711 if url[len(url)-1] != '/' {
712 localRedirect(w, r, path.Base(url)+"/")
713 return
714 }
715 } else if url[len(url)-1] == '/' {
716 base := path.Base(url)
717 if base == "/" || base == "." {
718
719 msg := "http: attempting to traverse a non-directory"
720 serveError(w, msg, StatusInternalServerError)
721 return
722 }
723 localRedirect(w, r, "../"+base)
724 return
725 }
726 }
727
728 if d.IsDir() {
729 url := r.URL.Path
730
731 if url == "" || url[len(url)-1] != '/' {
732 localRedirect(w, r, path.Base(url)+"/")
733 return
734 }
735
736
737 index := strings.TrimSuffix(name, "/") + indexPage
738 ff, err := fs.Open(index)
739 if err == nil {
740 defer ff.Close()
741 dd, err := ff.Stat()
742 if err == nil {
743 d = dd
744 f = ff
745 }
746 }
747 }
748
749
750 if d.IsDir() {
751 if checkIfModifiedSince(r, d.ModTime()) == condFalse {
752 writeNotModified(w)
753 return
754 }
755 setLastModified(w, d.ModTime())
756 dirList(w, r, f)
757 return
758 }
759
760
761 sizeFunc := func() (int64, error) { return d.Size(), nil }
762 serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f)
763 }
764
765
766
767
768
769
770 func toHTTPError(err error) (msg string, httpStatus int) {
771 if errors.Is(err, fs.ErrNotExist) {
772 return "404 page not found", StatusNotFound
773 }
774 if errors.Is(err, fs.ErrPermission) {
775 return "403 Forbidden", StatusForbidden
776 }
777 if errors.Is(err, errInvalidUnsafePath) {
778 return "404 page not found", StatusNotFound
779 }
780
781 return "500 Internal Server Error", StatusInternalServerError
782 }
783
784
785
786 func localRedirect(w ResponseWriter, r *Request, newPath string) {
787 if q := r.URL.RawQuery; q != "" {
788 newPath += "?" + q
789 }
790 w.Header().Set("Location", newPath)
791 w.WriteHeader(StatusMovedPermanently)
792 }
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815 func ServeFile(w ResponseWriter, r *Request, name string) {
816 if containsDotDot(r.URL.Path) {
817
818
819
820
821
822 serveError(w, "invalid URL path", StatusBadRequest)
823 return
824 }
825 dir, file := filepath.Split(name)
826 serveFile(w, r, Dir(dir), file, false)
827 }
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849 func ServeFileFS(w ResponseWriter, r *Request, fsys fs.FS, name string) {
850 if containsDotDot(r.URL.Path) {
851
852
853
854
855
856 serveError(w, "invalid URL path", StatusBadRequest)
857 return
858 }
859 serveFile(w, r, FS(fsys), name, false)
860 }
861
862 func containsDotDot(v string) bool {
863 if !strings.Contains(v, "..") {
864 return false
865 }
866 for ent := range strings.FieldsFuncSeq(v, isSlashRune) {
867 if ent == ".." {
868 return true
869 }
870 }
871 return false
872 }
873
874 func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
875
876 type fileHandler struct {
877 root FileSystem
878 }
879
880 type ioFS struct {
881 fsys fs.FS
882 }
883
884 type ioFile struct {
885 file fs.File
886 }
887
888 func (f ioFS) Open(name string) (File, error) {
889 if name == "/" {
890 name = "."
891 } else {
892 name = strings.TrimPrefix(name, "/")
893 }
894 file, err := f.fsys.Open(name)
895 if err != nil {
896 return nil, mapOpenError(err, name, '/', func(path string) (fs.FileInfo, error) {
897 return fs.Stat(f.fsys, path)
898 })
899 }
900 return ioFile{file}, nil
901 }
902
903 func (f ioFile) Close() error { return f.file.Close() }
904 func (f ioFile) Read(b []byte) (int, error) { return f.file.Read(b) }
905 func (f ioFile) Stat() (fs.FileInfo, error) { return f.file.Stat() }
906
907 var errMissingSeek = errors.New("io.File missing Seek method")
908 var errMissingReadDir = errors.New("io.File directory missing ReadDir method")
909
910 func (f ioFile) Seek(offset int64, whence int) (int64, error) {
911 s, ok := f.file.(io.Seeker)
912 if !ok {
913 return 0, errMissingSeek
914 }
915 return s.Seek(offset, whence)
916 }
917
918 func (f ioFile) ReadDir(count int) ([]fs.DirEntry, error) {
919 d, ok := f.file.(fs.ReadDirFile)
920 if !ok {
921 return nil, errMissingReadDir
922 }
923 return d.ReadDir(count)
924 }
925
926 func (f ioFile) Readdir(count int) ([]fs.FileInfo, error) {
927 d, ok := f.file.(fs.ReadDirFile)
928 if !ok {
929 return nil, errMissingReadDir
930 }
931 var list []fs.FileInfo
932 for {
933 dirs, err := d.ReadDir(count - len(list))
934 for _, dir := range dirs {
935 info, err := dir.Info()
936 if err != nil {
937
938 continue
939 }
940 list = append(list, info)
941 }
942 if err != nil {
943 return list, err
944 }
945 if count < 0 || len(list) >= count {
946 break
947 }
948 }
949 return list, nil
950 }
951
952
953
954
955 func FS(fsys fs.FS) FileSystem {
956 return ioFS{fsys}
957 }
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972 func FileServer(root FileSystem) Handler {
973 return &fileHandler{root}
974 }
975
976
977
978
979
980
981
982
983
984
985 func FileServerFS(root fs.FS) Handler {
986 return FileServer(FS(root))
987 }
988
989 func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
990 upath := r.URL.Path
991 if !strings.HasPrefix(upath, "/") {
992 upath = "/" + upath
993 r.URL.Path = upath
994 }
995 serveFile(w, r, f.root, path.Clean(upath), true)
996 }
997
998
999 type httpRange struct {
1000 start, length int64
1001 }
1002
1003 func (r httpRange) contentRange(size int64) string {
1004 return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size)
1005 }
1006
1007 func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader {
1008 return textproto.MIMEHeader{
1009 "Content-Range": {r.contentRange(size)},
1010 "Content-Type": {contentType},
1011 }
1012 }
1013
1014
1015
1016 func parseRange(s string, size int64) ([]httpRange, error) {
1017 if s == "" {
1018 return nil, nil
1019 }
1020 const b = "bytes="
1021 if !strings.HasPrefix(s, b) {
1022 return nil, errors.New("invalid range")
1023 }
1024 var ranges []httpRange
1025 noOverlap := false
1026 for ra := range strings.SplitSeq(s[len(b):], ",") {
1027 ra = textproto.TrimString(ra)
1028 if ra == "" {
1029 continue
1030 }
1031 start, end, ok := strings.Cut(ra, "-")
1032 if !ok {
1033 return nil, errors.New("invalid range")
1034 }
1035 start, end = textproto.TrimString(start), textproto.TrimString(end)
1036 var r httpRange
1037 if start == "" {
1038
1039
1040
1041
1042
1043 if end == "" || end[0] == '-' {
1044 return nil, errors.New("invalid range")
1045 }
1046 i, err := strconv.ParseInt(end, 10, 64)
1047 if i < 0 || err != nil {
1048 return nil, errors.New("invalid range")
1049 }
1050 if i > size {
1051 i = size
1052 }
1053 r.start = size - i
1054 r.length = size - r.start
1055 } else {
1056 i, err := strconv.ParseInt(start, 10, 64)
1057 if err != nil || i < 0 {
1058 return nil, errors.New("invalid range")
1059 }
1060 if i >= size {
1061
1062
1063 noOverlap = true
1064 continue
1065 }
1066 r.start = i
1067 if end == "" {
1068
1069 r.length = size - r.start
1070 } else {
1071 i, err := strconv.ParseInt(end, 10, 64)
1072 if err != nil || r.start > i {
1073 return nil, errors.New("invalid range")
1074 }
1075 if i >= size {
1076 i = size - 1
1077 }
1078 r.length = i - r.start + 1
1079 }
1080 }
1081 ranges = append(ranges, r)
1082 }
1083 if noOverlap && len(ranges) == 0 {
1084
1085 return nil, errNoOverlap
1086 }
1087 return ranges, nil
1088 }
1089
1090
1091 type countingWriter int64
1092
1093 func (w *countingWriter) Write(p []byte) (n int, err error) {
1094 *w += countingWriter(len(p))
1095 return len(p), nil
1096 }
1097
1098
1099
1100 func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) {
1101 var w countingWriter
1102 mw := multipart.NewWriter(&w)
1103 for _, ra := range ranges {
1104 mw.CreatePart(ra.mimeHeader(contentType, contentSize))
1105 encSize += ra.length
1106 }
1107 mw.Close()
1108 encSize += int64(w)
1109 return
1110 }
1111
1112 func sumRangesSize(ranges []httpRange) (size int64) {
1113 for _, ra := range ranges {
1114 size += ra.length
1115 }
1116 return
1117 }
1118
View as plain text