~ubuntu-branches/ubuntu/saucy/juju-core/saucy

« back to all changes in this revision

Viewing changes to src/launchpad.net/gwacl/fork/http/fs.go

  • Committer: Package Import Robot
  • Author(s): James Page
  • Date: 2013-07-23 08:51:44 UTC
  • mfrom: (1.1.3)
  • Revision ID: package-import@ubuntu.com-20130723085144-0oty5omapea7t8xt
Tags: 1.11.4-0ubuntu1
* New upstream release:
  - d/copyright: Drop section for go-curl.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2009 The Go Authors. All rights reserved.
 
2
// Use of this source code is governed by a BSD-style
 
3
// license that can be found in the LICENSE file.
 
4
 
 
5
// HTTP file system request handler
 
6
 
 
7
package http
 
8
 
 
9
import (
 
10
    "errors"
 
11
    "fmt"
 
12
    "io"
 
13
    "mime"
 
14
    "os"
 
15
    "path"
 
16
    "path/filepath"
 
17
    "strconv"
 
18
    "strings"
 
19
    "time"
 
20
)
 
21
 
 
22
// A Dir implements http.FileSystem using the native file
 
23
// system restricted to a specific directory tree.
 
24
//
 
25
// An empty Dir is treated as ".".
 
26
type Dir string
 
27
 
 
28
func (d Dir) Open(name string) (File, error) {
 
29
    if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 {
 
30
        return nil, errors.New("http: invalid character in file path")
 
31
    }
 
32
    dir := string(d)
 
33
    if dir == "" {
 
34
        dir = "."
 
35
    }
 
36
    f, err := os.Open(filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))))
 
37
    if err != nil {
 
38
        return nil, err
 
39
    }
 
40
    return f, nil
 
41
}
 
42
 
 
43
// A FileSystem implements access to a collection of named files.
 
44
// The elements in a file path are separated by slash ('/', U+002F)
 
45
// characters, regardless of host operating system convention.
 
46
type FileSystem interface {
 
47
    Open(name string) (File, error)
 
48
}
 
49
 
 
50
// A File is returned by a FileSystem's Open method and can be
 
51
// served by the FileServer implementation.
 
52
type File interface {
 
53
    Close() error
 
54
    Stat() (os.FileInfo, error)
 
55
    Readdir(count int) ([]os.FileInfo, error)
 
56
    Read([]byte) (int, error)
 
57
    Seek(offset int64, whence int) (int64, error)
 
58
}
 
59
 
 
60
func dirList(w ResponseWriter, f File) {
 
61
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
 
62
    fmt.Fprintf(w, "<pre>\n")
 
63
    for {
 
64
        dirs, err := f.Readdir(100)
 
65
        if err != nil || len(dirs) == 0 {
 
66
            break
 
67
        }
 
68
        for _, d := range dirs {
 
69
            name := d.Name()
 
70
            if d.IsDir() {
 
71
                name += "/"
 
72
            }
 
73
            // TODO htmlescape
 
74
            fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", name, name)
 
75
        }
 
76
    }
 
77
    fmt.Fprintf(w, "</pre>\n")
 
78
}
 
79
 
 
80
// ServeContent replies to the request using the content in the
 
81
// provided ReadSeeker.  The main benefit of ServeContent over io.Copy
 
82
// is that it handles Range requests properly, sets the MIME type, and
 
83
// handles If-Modified-Since requests.
 
84
//
 
85
// If the response's Content-Type header is not set, ServeContent
 
86
// first tries to deduce the type from name's file extension and,
 
87
// if that fails, falls back to reading the first block of the content
 
88
// and passing it to DetectContentType.
 
89
// The name is otherwise unused; in particular it can be empty and is
 
90
// never sent in the response.
 
91
//
 
92
// If modtime is not the zero time, ServeContent includes it in a
 
93
// Last-Modified header in the response.  If the request includes an
 
94
// If-Modified-Since header, ServeContent uses modtime to decide
 
95
// whether the content needs to be sent at all.
 
96
//
 
97
// The content's Seek method must work: ServeContent uses
 
98
// a seek to the end of the content to determine its size.
 
99
//
 
100
// Note that *os.File implements the io.ReadSeeker interface.
 
101
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) {
 
102
    size, err := content.Seek(0, os.SEEK_END)
 
103
    if err != nil {
 
104
        Error(w, "seeker can't seek", StatusInternalServerError)
 
105
        return
 
106
    }
 
107
    _, err = content.Seek(0, os.SEEK_SET)
 
108
    if err != nil {
 
109
        Error(w, "seeker can't seek", StatusInternalServerError)
 
110
        return
 
111
    }
 
112
    serveContent(w, req, name, modtime, size, content)
 
113
}
 
114
 
 
115
// if name is empty, filename is unknown. (used for mime type, before sniffing)
 
116
// if modtime.IsZero(), modtime is unknown.
 
117
// content must be seeked to the beginning of the file.
 
118
func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, size int64, content io.ReadSeeker) {
 
119
    if checkLastModified(w, r, modtime) {
 
120
        return
 
121
    }
 
122
 
 
123
    code := StatusOK
 
124
 
 
125
    // If Content-Type isn't set, use the file's extension to find it.
 
126
    if w.Header().Get("Content-Type") == "" {
 
127
        ctype := mime.TypeByExtension(filepath.Ext(name))
 
128
        if ctype == "" {
 
129
            // read a chunk to decide between utf-8 text and binary
 
130
            var buf [1024]byte
 
131
            n, _ := io.ReadFull(content, buf[:])
 
132
            b := buf[:n]
 
133
            ctype = DetectContentType(b)
 
134
            _, err := content.Seek(0, os.SEEK_SET) // rewind to output whole file
 
135
            if err != nil {
 
136
                Error(w, "seeker can't seek", StatusInternalServerError)
 
137
                return
 
138
            }
 
139
        }
 
140
        w.Header().Set("Content-Type", ctype)
 
141
    }
 
142
 
 
143
    // handle Content-Range header.
 
144
    // TODO(adg): handle multiple ranges
 
145
    sendSize := size
 
146
    if size >= 0 {
 
147
        ranges, err := parseRange(r.Header.Get("Range"), size)
 
148
        if err == nil && len(ranges) > 1 {
 
149
            err = errors.New("multiple ranges not supported")
 
150
        }
 
151
        if err != nil {
 
152
            Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
 
153
            return
 
154
        }
 
155
        if len(ranges) == 1 {
 
156
            ra := ranges[0]
 
157
            if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil {
 
158
                Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
 
159
                return
 
160
            }
 
161
            sendSize = ra.length
 
162
            code = StatusPartialContent
 
163
            w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ra.start, ra.start+ra.length-1, size))
 
164
        }
 
165
 
 
166
        w.Header().Set("Accept-Ranges", "bytes")
 
167
        if w.Header().Get("Content-Encoding") == "" {
 
168
            w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
 
169
        }
 
170
    }
 
171
 
 
172
    w.WriteHeader(code)
 
173
 
 
174
    if r.Method != "HEAD" {
 
175
        if sendSize == -1 {
 
176
            io.Copy(w, content)
 
177
        } else {
 
178
            io.CopyN(w, content, sendSize)
 
179
        }
 
180
    }
 
181
}
 
182
 
 
183
// modtime is the modification time of the resource to be served, or IsZero().
 
184
// return value is whether this request is now complete.
 
185
func checkLastModified(w ResponseWriter, r *Request, modtime time.Time) bool {
 
186
    if modtime.IsZero() {
 
187
        return false
 
188
    }
 
189
 
 
190
    // The Date-Modified header truncates sub-second precision, so
 
191
    // use mtime < t+1s instead of mtime <= t to check for unmodified.
 
192
    if t, err := time.Parse(TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) {
 
193
        w.WriteHeader(StatusNotModified)
 
194
        return true
 
195
    }
 
196
    w.Header().Set("Last-Modified", modtime.UTC().Format(TimeFormat))
 
197
    return false
 
198
}
 
199
 
 
200
// name is '/'-separated, not filepath.Separator.
 
201
func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) {
 
202
    const indexPage = "/index.html"
 
203
 
 
204
    // redirect .../index.html to .../
 
205
    // can't use Redirect() because that would make the path absolute,
 
206
    // which would be a problem running under StripPrefix
 
207
    if strings.HasSuffix(r.URL.Path, indexPage) {
 
208
        localRedirect(w, r, "./")
 
209
        return
 
210
    }
 
211
 
 
212
    f, err := fs.Open(name)
 
213
    if err != nil {
 
214
        // TODO expose actual error?
 
215
        NotFound(w, r)
 
216
        return
 
217
    }
 
218
    defer f.Close()
 
219
 
 
220
    d, err1 := f.Stat()
 
221
    if err1 != nil {
 
222
        // TODO expose actual error?
 
223
        NotFound(w, r)
 
224
        return
 
225
    }
 
226
 
 
227
    if redirect {
 
228
        // redirect to canonical path: / at end of directory url
 
229
        // r.URL.Path always begins with /
 
230
        url := r.URL.Path
 
231
        if d.IsDir() {
 
232
            if url[len(url)-1] != '/' {
 
233
                localRedirect(w, r, path.Base(url)+"/")
 
234
                return
 
235
            }
 
236
        } else {
 
237
            if url[len(url)-1] == '/' {
 
238
                localRedirect(w, r, "../"+path.Base(url))
 
239
                return
 
240
            }
 
241
        }
 
242
    }
 
243
 
 
244
    // use contents of index.html for directory, if present
 
245
    if d.IsDir() {
 
246
        if checkLastModified(w, r, d.ModTime()) {
 
247
            return
 
248
        }
 
249
        index := name + indexPage
 
250
        ff, err := fs.Open(index)
 
251
        if err == nil {
 
252
            defer ff.Close()
 
253
            dd, err := ff.Stat()
 
254
            if err == nil {
 
255
                name = index
 
256
                d = dd
 
257
                f = ff
 
258
            }
 
259
        }
 
260
    }
 
261
 
 
262
    if d.IsDir() {
 
263
        dirList(w, f)
 
264
        return
 
265
    }
 
266
 
 
267
    serveContent(w, r, d.Name(), d.ModTime(), d.Size(), f)
 
268
}
 
269
 
 
270
// localRedirect gives a Moved Permanently response.
 
271
// It does not convert relative paths to absolute paths like Redirect does.
 
272
func localRedirect(w ResponseWriter, r *Request, newPath string) {
 
273
    if q := r.URL.RawQuery; q != "" {
 
274
        newPath += "?" + q
 
275
    }
 
276
    w.Header().Set("Location", newPath)
 
277
    w.WriteHeader(StatusMovedPermanently)
 
278
}
 
279
 
 
280
// ServeFile replies to the request with the contents of the named file or directory.
 
281
func ServeFile(w ResponseWriter, r *Request, name string) {
 
282
    dir, file := filepath.Split(name)
 
283
    serveFile(w, r, Dir(dir), file, false)
 
284
}
 
285
 
 
286
type fileHandler struct {
 
287
    root FileSystem
 
288
}
 
289
 
 
290
// FileServer returns a handler that serves HTTP requests
 
291
// with the contents of the file system rooted at root.
 
292
//
 
293
// To use the operating system's file system implementation,
 
294
// use http.Dir:
 
295
//
 
296
//     http.Handle("/", http.FileServer(http.Dir("/tmp")))
 
297
func FileServer(root FileSystem) Handler {
 
298
    return &fileHandler{root}
 
299
}
 
300
 
 
301
func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) {
 
302
    upath := r.URL.Path
 
303
    if !strings.HasPrefix(upath, "/") {
 
304
        upath = "/" + upath
 
305
        r.URL.Path = upath
 
306
    }
 
307
    serveFile(w, r, f.root, path.Clean(upath), true)
 
308
}
 
309
 
 
310
// httpRange specifies the byte range to be sent to the client.
 
311
type httpRange struct {
 
312
    start, length int64
 
313
}
 
314
 
 
315
// parseRange parses a Range header string as per RFC 2616.
 
316
func parseRange(s string, size int64) ([]httpRange, error) {
 
317
    if s == "" {
 
318
        return nil, nil // header not present
 
319
    }
 
320
    const b = "bytes="
 
321
    if !strings.HasPrefix(s, b) {
 
322
        return nil, errors.New("invalid range")
 
323
    }
 
324
    var ranges []httpRange
 
325
    for _, ra := range strings.Split(s[len(b):], ",") {
 
326
        i := strings.Index(ra, "-")
 
327
        if i < 0 {
 
328
            return nil, errors.New("invalid range")
 
329
        }
 
330
        start, end := ra[:i], ra[i+1:]
 
331
        var r httpRange
 
332
        if start == "" {
 
333
            // If no start is specified, end specifies the
 
334
            // range start relative to the end of the file.
 
335
            i, err := strconv.ParseInt(end, 10, 64)
 
336
            if err != nil {
 
337
                return nil, errors.New("invalid range")
 
338
            }
 
339
            if i > size {
 
340
                i = size
 
341
            }
 
342
            r.start = size - i
 
343
            r.length = size - r.start
 
344
        } else {
 
345
            i, err := strconv.ParseInt(start, 10, 64)
 
346
            if err != nil || i > size || i < 0 {
 
347
                return nil, errors.New("invalid range")
 
348
            }
 
349
            r.start = i
 
350
            if end == "" {
 
351
                // If no end is specified, range extends to end of the file.
 
352
                r.length = size - r.start
 
353
            } else {
 
354
                i, err := strconv.ParseInt(end, 10, 64)
 
355
                if err != nil || r.start > i {
 
356
                    return nil, errors.New("invalid range")
 
357
                }
 
358
                if i >= size {
 
359
                    i = size - 1
 
360
                }
 
361
                r.length = i - r.start + 1
 
362
            }
 
363
        }
 
364
        ranges = append(ranges, r)
 
365
    }
 
366
    return ranges, nil
 
367
}