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