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