mirror of
				https://github.com/rclone/rclone.git
				synced 2025-10-30 23:17:59 +02:00 
			
		
		
		
	http: fix, tidy and rework ready for release
* Fix remaining problems * Refactor to make testing easier and add a test suite * Make path parsing more robust. * Add single file operations * Add MimeType reading for objects * Add documentation * Note go1.7+ is required to build
This commit is contained in:
		| @@ -27,6 +27,7 @@ Rclone is a command line program to sync files and directories to and from | ||||
|   * Yandex Disk | ||||
|   * SFTP | ||||
|   * FTP | ||||
|   * HTTP | ||||
|   * The local filesystem | ||||
|  | ||||
| Features | ||||
|   | ||||
| @@ -30,8 +30,9 @@ docs = [ | ||||
|     "b2.md", | ||||
|     "yandex.md", | ||||
|     "sftp.md", | ||||
|     "crypt.md", | ||||
|     "ftp.md", | ||||
|     "http.md", | ||||
|     "crypt.md", | ||||
|     "local.md", | ||||
|     "changelog.md", | ||||
|     "bugs.md", | ||||
|   | ||||
| @@ -53,6 +53,7 @@ from various cloud storage systems and using file transfer services, such as: | ||||
|   * Yandex Disk | ||||
|   * SFTP | ||||
|   * FTP | ||||
|   * HTTP | ||||
|   * The local filesystem | ||||
|  | ||||
| Features | ||||
|   | ||||
| @@ -25,6 +25,7 @@ Rclone is a command line program to sync files and directories to and from | ||||
|   * Yandex Disk | ||||
|   * SFTP | ||||
|   * FTP | ||||
|   * HTTP | ||||
|   * The local filesystem | ||||
|  | ||||
| Features | ||||
|   | ||||
| @@ -32,6 +32,7 @@ See the following for detailed instructions for | ||||
|   * [Yandex Disk](/yandex/) | ||||
|   * [SFTP](/sftp/) | ||||
|   * [FTP](/ftp/) | ||||
|   * [HTTP](/http/) | ||||
|   * [Crypt](/crypt/) - to encrypt other remotes | ||||
|  | ||||
| Usage | ||||
|   | ||||
							
								
								
									
										137
									
								
								docs/content/http.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								docs/content/http.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| --- | ||||
| title: "HTTP Remote" | ||||
| description: "Read only remote for HTTP servers" | ||||
| date: "2017-06-19" | ||||
| --- | ||||
|  | ||||
| <i class="fa fa-globe"></i> HTTP | ||||
| ------------------------------------------------- | ||||
|  | ||||
| The HTTP remote is a read only remote for reading files of a | ||||
| webserver.  The webserver should provide file listings which rclone | ||||
| will read and turn into a remote.  This has been tested with common | ||||
| webservers such as Apache/Nginx/Caddy and will likely work with file | ||||
| listings from most web servers.  (If it doesn't then please file an | ||||
| issue, or send a pull request!) | ||||
|  | ||||
| Paths are specified as `remote:` or `remote:path/to/dir`. | ||||
|  | ||||
| Here is an example of how to make a remote called `remote`.  First | ||||
| run: | ||||
|  | ||||
|      rclone config | ||||
|  | ||||
| This will guide you through an interactive setup process: | ||||
|  | ||||
| ``` | ||||
| No remotes found - make a new one | ||||
| n) New remote | ||||
| s) Set configuration password | ||||
| q) Quit config | ||||
| n/s/q> n | ||||
| name> remote | ||||
| Type of storage to configure. | ||||
| Choose a number from below, or type in your own value | ||||
|  1 / Amazon Drive | ||||
|    \ "amazon cloud drive" | ||||
|  2 / Amazon S3 (also Dreamhost, Ceph, Minio) | ||||
|    \ "s3" | ||||
|  3 / Backblaze B2 | ||||
|    \ "b2" | ||||
|  4 / Dropbox | ||||
|    \ "dropbox" | ||||
|  5 / Encrypt/Decrypt a remote | ||||
|    \ "crypt" | ||||
|  6 / FTP Connection | ||||
|    \ "ftp" | ||||
|  7 / Google Cloud Storage (this is not Google Drive) | ||||
|    \ "google cloud storage" | ||||
|  8 / Google Drive | ||||
|    \ "drive" | ||||
|  9 / Hubic | ||||
|    \ "hubic" | ||||
| 10 / Local Disk | ||||
|    \ "local" | ||||
| 11 / Microsoft OneDrive | ||||
|    \ "onedrive" | ||||
| 12 / Openstack Swift (Rackspace Cloud Files, Memset Memstore, OVH) | ||||
|    \ "swift" | ||||
| 13 / SSH/SFTP Connection | ||||
|    \ "sftp" | ||||
| 14 / Yandex Disk | ||||
|    \ "yandex" | ||||
| 15 / http Connection | ||||
|    \ "http" | ||||
| Storage> http | ||||
| URL of http host to connect to | ||||
| Choose a number from below, or type in your own value | ||||
|  1 / Connect to example.com | ||||
|    \ "https://example.com" | ||||
| url> https://beta.rclone.org | ||||
| Remote config | ||||
| -------------------- | ||||
| [remote] | ||||
| url = https://beta.rclone.org | ||||
| -------------------- | ||||
| y) Yes this is OK | ||||
| e) Edit this remote | ||||
| d) Delete this remote | ||||
| y/e/d> y | ||||
| Current remotes: | ||||
|  | ||||
| Name                 Type | ||||
| ====                 ==== | ||||
| remote               http | ||||
|  | ||||
| e) Edit existing remote | ||||
| n) New remote | ||||
| d) Delete remote | ||||
| r) Rename remote | ||||
| c) Copy remote | ||||
| s) Set configuration password | ||||
| q) Quit config | ||||
| e/n/d/r/c/s/q> q | ||||
| ``` | ||||
|  | ||||
| This remote is called `remote` and can now be used like this | ||||
|  | ||||
| See all the top level directories | ||||
|  | ||||
|     rclone lsd remote: | ||||
|  | ||||
| List the contents of a directory | ||||
|  | ||||
|     rclone ls remote:directory | ||||
|  | ||||
| Sync the remote `directory` to `/home/local/directory`, deleting any excess files. | ||||
|  | ||||
|     rclone sync remote:directory /home/local/directory | ||||
|  | ||||
| ### Read only ### | ||||
|  | ||||
| This remote is read only - you can't upload files to an HTTP server. | ||||
|  | ||||
| ### Modified time ### | ||||
|  | ||||
| Most HTTP servers store time accurate to 1 second. | ||||
|  | ||||
| ### Checksum ### | ||||
|  | ||||
| No checksums are stored. | ||||
|  | ||||
| ### Usage without a config file ### | ||||
|  | ||||
| Note that since only two environment variable need to be set, it is | ||||
| easy to use without a config file like this. | ||||
|  | ||||
| ``` | ||||
| RCLONE_CONFIG_ZZ_TYPE=http RCLONE_CONFIG_ZZ_URL=https://beta.rclone.org rclone lsd zz: | ||||
| ``` | ||||
|  | ||||
| Or if you prefer | ||||
|  | ||||
| ``` | ||||
| export RCLONE_CONFIG_ZZ_TYPE=http | ||||
| export RCLONE_CONFIG_ZZ_URL=https://beta.rclone.org | ||||
| rclone lsd zz: | ||||
| ``` | ||||
| @@ -29,6 +29,7 @@ Here is an overview of the major features of each cloud storage system. | ||||
| | Yandex Disk            | MD5     | Yes     | No               | No              | R/W       | | ||||
| | SFTP                   | -       | Yes     | Depends          | No              | -         | | ||||
| | FTP                    | -       | No      | Yes              | No              | -         | | ||||
| | HTTP                   | -       | No      | Yes              | No              | R         | | ||||
| | The local filesystem   | All     | Yes     | Depends          | No              | -         | | ||||
|  | ||||
| ### Hash ### | ||||
| @@ -122,6 +123,7 @@ operations more efficient. | ||||
| | Yandex Disk            | Yes   | No   | No   | No      | No  [#575](https://github.com/ncw/rclone/issues/575) | Yes   | | ||||
| | SFTP                   | No    | No   | Yes  | Yes     | No      | No    | | ||||
| | FTP                    | No    | No   | Yes  | Yes     | No      | No    | | ||||
| | HTTP                   | No    | No   | No   | No      | No      | No    | | ||||
| | The local filesystem   | Yes   | No   | Yes  | Yes     | No      | No    | | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -62,6 +62,7 @@ | ||||
|                     <li><a href="/yandex/"><i class="fa fa-space-shuttle"></i> Yandex Disk</a></li> | ||||
|                     <li><a href="/sftp/"><i class="fa fa-server"></i> SFTP</a></li> | ||||
|                     <li><a href="/ftp/"><i class="fa fa-file"></i> FTP</a></li> | ||||
|                     <li><a href="/http/"><i class="fa fa-globe"></i> HTTP</a></li> | ||||
|                     <li><a href="/crypt/"><i class="fa fa-lock"></i> Crypt (encrypts the above)</a></li> | ||||
|                   </ul> | ||||
|                 </li> | ||||
|   | ||||
							
								
								
									
										422
									
								
								http/http.go
									
									
									
									
									
								
							
							
						
						
									
										422
									
								
								http/http.go
									
									
									
									
									
								
							| @@ -1,18 +1,16 @@ | ||||
| // Package http provides a filesystem interface using golang.org/net/http | ||||
| // | ||||
| // It treads HTML pages served from the endpoint as directory | ||||
| // It treats HTML pages served from the endpoint as directory | ||||
| // listings, and includes any links found as files. | ||||
|  | ||||
| // +build !plan9 | ||||
| // +build go1.7 | ||||
|  | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| @@ -23,7 +21,10 @@ import ( | ||||
| 	"golang.org/x/net/html" | ||||
| ) | ||||
|  | ||||
| var errorReadOnly = errors.New("http remotes are read only") | ||||
| var ( | ||||
| 	errorReadOnly = errors.New("http remotes are read only") | ||||
| 	timeUnset     = time.Unix(0, 0) | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	fsi := &fs.RegInfo{ | ||||
| @@ -31,7 +32,7 @@ func init() { | ||||
| 		Description: "http Connection", | ||||
| 		NewFs:       NewFs, | ||||
| 		Options: []fs.Option{{ | ||||
| 			Name:     "endpoint", | ||||
| 			Name:     "url", | ||||
| 			Help:     "URL of http host to connect to", | ||||
| 			Optional: false, | ||||
| 			Examples: []fs.OptionExample{{ | ||||
| @@ -54,49 +55,86 @@ type Fs struct { | ||||
|  | ||||
| // Object is a remote object that has been stat'd (so it exists, but is not necessarily open for reading) | ||||
| type Object struct { | ||||
| 	fs     *Fs | ||||
| 	remote string | ||||
| 	info   os.FileInfo | ||||
| 	fs          *Fs | ||||
| 	remote      string | ||||
| 	size        int64 | ||||
| 	modTime     time.Time | ||||
| 	contentType string | ||||
| } | ||||
|  | ||||
| // ObjectReader holds the File interface to a remote http file opened for reading | ||||
| type ObjectReader struct { | ||||
| 	object   *Object | ||||
| 	httpFile io.ReadCloser | ||||
| } | ||||
|  | ||||
| func urlJoin(u *url.URL, paths ...string) string { | ||||
| 	r := u | ||||
| 	for _, p := range paths { | ||||
| 		if p == "/" { | ||||
| 			continue | ||||
| 		} | ||||
| 		rel, _ := url.Parse(p) | ||||
| 		r = r.ResolveReference(rel) | ||||
| // Join a URL and a path returning a new URL | ||||
| func urlJoin(base *url.URL, path string) *url.URL { | ||||
| 	rel, err := url.Parse(path) | ||||
| 	if err != nil { | ||||
| 		fs.Errorf(nil, "Error parsing %q as URL: %v", path, err) | ||||
| 	} | ||||
| 	return r.String() | ||||
| 	return base.ResolveReference(rel) | ||||
| } | ||||
|  | ||||
| // statusError returns an error if the res contained an error | ||||
| func statusError(res *http.Response, err error) error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if res.StatusCode < 200 || res.StatusCode > 299 { | ||||
| 		_ = res.Body.Close() | ||||
| 		return errors.Errorf("HTTP Error %d: %s", res.StatusCode, res.Status) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // NewFs creates a new Fs object from the name and root. It connects to | ||||
| // the host specified in the config file. | ||||
| func NewFs(name, root string) (fs.Fs, error) { | ||||
| 	endpoint := fs.ConfigFileGet(name, "endpoint") | ||||
| 	endpoint := fs.ConfigFileGet(name, "url") | ||||
| 	if !strings.HasSuffix(endpoint, "/") { | ||||
| 		endpoint += "/" | ||||
| 	} | ||||
|  | ||||
| 	u, err := url.Parse(endpoint) | ||||
| 	// Parse the endpoint and stick the root onto it | ||||
| 	base, err := url.Parse(endpoint) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	rootURL, err := url.Parse(root) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	u := base.ResolveReference(rootURL) | ||||
|  | ||||
| 	client := fs.Config.Client() | ||||
|  | ||||
| 	var isFile = false | ||||
| 	if !strings.HasSuffix(u.String(), "/") { | ||||
| 		// Make a client which doesn't follow redirects so the server | ||||
| 		// doesn't redirect http://host/dir to http://host/dir/ | ||||
| 		noRedir := *client | ||||
| 		noRedir.CheckRedirect = func(req *http.Request, via []*http.Request) error { | ||||
| 			return http.ErrUseLastResponse | ||||
| 		} | ||||
| 		// check to see if points to a file | ||||
| 		res, err := noRedir.Head(u.String()) | ||||
| 		err = statusError(res, err) | ||||
| 		if err == nil { | ||||
| 			isFile = true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	newRoot := u.String() | ||||
| 	if isFile { | ||||
| 		// Point to the parent if this is a file | ||||
| 		newRoot, _ = path.Split(u.String()) | ||||
| 	} else { | ||||
| 		if !strings.HasSuffix(newRoot, "/") { | ||||
| 			newRoot += "/" | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	u, err = url.Parse(newRoot) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if !strings.HasSuffix(root, "/") && root != "" { | ||||
| 		root += "/" | ||||
| 	} | ||||
|  | ||||
| 	client := fs.Config.Client() | ||||
|  | ||||
| 	_, err = client.Head(urlJoin(u, root)) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "couldn't connect http") | ||||
| 	} | ||||
| 	f := &Fs{ | ||||
| 		name:       name, | ||||
| 		root:       root, | ||||
| @@ -104,6 +142,9 @@ func NewFs(name, root string) (fs.Fs, error) { | ||||
| 		endpoint:   u, | ||||
| 	} | ||||
| 	f.features = (&fs.Features{}).Fill(f) | ||||
| 	if isFile { | ||||
| 		return f, fs.ErrorIsFile | ||||
| 	} | ||||
| 	return f, nil | ||||
| } | ||||
|  | ||||
| @@ -119,7 +160,7 @@ func (f *Fs) Root() string { | ||||
|  | ||||
| // String returns the URL for the filesystem | ||||
| func (f *Fs) String() string { | ||||
| 	return urlJoin(f.endpoint, f.root) | ||||
| 	return f.endpoint.String() | ||||
| } | ||||
|  | ||||
| // Features returns the optional features of this Fs | ||||
| @@ -145,51 +186,6 @@ func (f *Fs) NewObject(remote string) (fs.Object, error) { | ||||
| 	return o, nil | ||||
| } | ||||
|  | ||||
| // dirExists returns true,nil if the directory exists, false, nil if | ||||
| // it doesn't or false, err | ||||
| func (f *Fs) dirExists(dir string) (bool, error) { | ||||
| 	res, err := f.httpClient.Head(urlJoin(f.endpoint, dir)) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 	if res.StatusCode == http.StatusOK { | ||||
| 		return true, nil | ||||
| 	} | ||||
| 	return false, nil | ||||
| } | ||||
|  | ||||
| type entry struct { | ||||
| 	name  string | ||||
| 	url   string | ||||
| 	size  int64 | ||||
| 	mode  os.FileMode | ||||
| 	mtime int64 | ||||
| } | ||||
|  | ||||
| func (e *entry) Name() string { | ||||
| 	return e.name | ||||
| } | ||||
|  | ||||
| func (e *entry) Size() int64 { | ||||
| 	return e.size | ||||
| } | ||||
|  | ||||
| func (e *entry) Mode() os.FileMode { | ||||
| 	return os.FileMode(e.mode) | ||||
| } | ||||
|  | ||||
| func (e *entry) ModTime() time.Time { | ||||
| 	return time.Unix(e.mtime, 0) | ||||
| } | ||||
|  | ||||
| func (e *entry) IsDir() bool { | ||||
| 	return e.mode&os.ModeDir != 0 | ||||
| } | ||||
|  | ||||
| func (e *entry) Sys() interface{} { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func parseInt64(s string) int64 { | ||||
| 	n, e := strconv.ParseInt(s, 10, 64) | ||||
| 	if e != nil { | ||||
| @@ -198,84 +194,95 @@ func parseInt64(s string) int64 { | ||||
| 	return n | ||||
| } | ||||
|  | ||||
| func parseBool(s string) bool { | ||||
| 	b, e := strconv.ParseBool(s) | ||||
| 	if e != nil { | ||||
| 		return false | ||||
| // parseName turns a name as found in the page into a remote path or returns false | ||||
| func parseName(base *url.URL, val string) (string, bool) { | ||||
| 	name, err := url.QueryUnescape(val) | ||||
| 	if err != nil { | ||||
| 		return "", false | ||||
| 	} | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| func prepareTimeString(ts string) string { | ||||
| 	return strings.Trim(strings.Join(strings.SplitN(strings.Trim(ts, "\t "), " ", 3)[0:2], " "), "\r\n\t ") | ||||
| } | ||||
|  | ||||
| func parseTime(n *html.Node) (t time.Time) { | ||||
| 	if ts := prepareTimeString(n.Data); ts != "" { | ||||
| 		t, _ = time.Parse("2-Jan-2006 15:04", ts) | ||||
| 	u := urlJoin(base, name) | ||||
| 	uStr := u.String() | ||||
| 	if strings.Index(uStr, "?") >= 0 { | ||||
| 		return "", false | ||||
| 	} | ||||
| 	return t | ||||
| 	baseStr := base.String() | ||||
| 	// check has URL prefix | ||||
| 	if !strings.HasPrefix(uStr, baseStr) { | ||||
| 		return "", false | ||||
| 	} | ||||
| 	// check has path prefix | ||||
| 	if !strings.HasPrefix(u.Path, base.Path) { | ||||
| 		return "", false | ||||
| 	} | ||||
| 	// calculate the name relative to the base | ||||
| 	name = u.Path[len(base.Path):] | ||||
| 	// musn't be empty | ||||
| 	if name == "" { | ||||
| 		return "", false | ||||
| 	} | ||||
| 	// mustn't contain a / | ||||
| 	slash := strings.Index(name, "/") | ||||
| 	if slash >= 0 && slash != len(name)-1 { | ||||
| 		return "", false | ||||
| 	} | ||||
| 	return name, true | ||||
| } | ||||
|  | ||||
| func (f *Fs) readDir(p string) ([]*entry, error) { | ||||
| 	entries := make([]*entry, 0) | ||||
| 	res, err := f.httpClient.Get(urlJoin(f.endpoint, p)) | ||||
| // Parse turns HTML for a directory into names | ||||
| // base should be the base URL to resolve any relative names from | ||||
| func parse(base *url.URL, in io.Reader) (names []string, err error) { | ||||
| 	doc, err := html.Parse(in) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if res.Body == nil || res.StatusCode != http.StatusOK { | ||||
| 		//return nil, errors.Errorf("directory listing failed with error: % (%d)", res.Status, res.StatusCode) | ||||
| 		return nil, nil | ||||
| 	var walk func(*html.Node) | ||||
| 	walk = func(n *html.Node) { | ||||
| 		if n.Type == html.ElementNode && n.Data == "a" { | ||||
| 			for _, a := range n.Attr { | ||||
| 				if a.Key == "href" { | ||||
| 					name, ok := parseName(base, a.Val) | ||||
| 					if ok { | ||||
| 						names = append(names, name) | ||||
| 					} | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		for c := n.FirstChild; c != nil; c = c.NextSibling { | ||||
| 			walk(c) | ||||
| 		} | ||||
| 	} | ||||
| 	walk(doc) | ||||
| 	return names, nil | ||||
| } | ||||
|  | ||||
| // Read the directory passed in | ||||
| func (f *Fs) readDir(dir string) (names []string, err error) { | ||||
| 	u := urlJoin(f.endpoint, dir) | ||||
| 	if !strings.HasSuffix(u.String(), "/") { | ||||
| 		return nil, errors.Errorf("internal error: readDir URL %q didn't end in /", u.String()) | ||||
| 	} | ||||
| 	res, err := f.httpClient.Get(u.String()) | ||||
| 	if err == nil && res.StatusCode == http.StatusNotFound { | ||||
| 		return nil, fs.ErrorDirNotFound | ||||
| 	} | ||||
| 	err = statusError(res, err) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "failed to readDir") | ||||
| 	} | ||||
| 	defer fs.CheckClose(res.Body, &err) | ||||
|  | ||||
| 	switch strings.SplitN(res.Header.Get("Content-Type"), ";", 2)[0] { | ||||
| 	contentType := strings.SplitN(res.Header.Get("Content-Type"), ";", 2)[0] | ||||
| 	switch contentType { | ||||
| 	case "text/html": | ||||
| 		doc, err := html.Parse(res.Body) | ||||
| 		names, err = parse(u, res.Body) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 			return nil, errors.Wrap(err, "readDir") | ||||
| 		} | ||||
| 		var walk func(*html.Node) | ||||
| 		walk = func(n *html.Node) { | ||||
| 			if n.Type == html.ElementNode && n.Data == "a" { | ||||
| 				for _, a := range n.Attr { | ||||
| 					if a.Key == "href" { | ||||
| 						name, err := url.QueryUnescape(a.Val) | ||||
| 						if err != nil { | ||||
| 							continue | ||||
| 						} | ||||
| 						if name == "../" || name == "./" || name == ".." { | ||||
| 							break | ||||
| 						} | ||||
| 						if strings.Index(name, "?") >= 0 || strings.HasPrefix(name, "http") { | ||||
| 							break | ||||
| 						} | ||||
| 						u, err := url.Parse(name) | ||||
| 						if err != nil { | ||||
| 							break | ||||
| 						} | ||||
| 						name = path.Clean(u.Path) | ||||
| 						e := &entry{ | ||||
| 							name: strings.TrimRight(name, "/"), | ||||
| 							url:  name, | ||||
| 						} | ||||
| 						if a.Val[len(a.Val)-1] == '/' { | ||||
| 							e.mode = os.FileMode(0555) | os.ModeDir | ||||
| 						} else { | ||||
| 							e.mode = os.FileMode(0444) | ||||
| 						} | ||||
| 						entries = append(entries, e) | ||||
| 						break | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 			for c := n.FirstChild; c != nil; c = c.NextSibling { | ||||
| 				walk(c) | ||||
| 			} | ||||
| 		} | ||||
| 		walk(doc) | ||||
| 	default: | ||||
| 		return nil, errors.Errorf("Can't parse content type %q", contentType) | ||||
| 	} | ||||
| 	return entries, nil | ||||
| 	return names, nil | ||||
| } | ||||
|  | ||||
| // List the objects and directories in dir into entries.  The | ||||
| @@ -288,36 +295,21 @@ func (f *Fs) readDir(p string) ([]*entry, error) { | ||||
| // This should return ErrDirNotFound if the directory isn't | ||||
| // found. | ||||
| func (f *Fs) List(dir string) (entries fs.DirEntries, err error) { | ||||
| 	endpoint := path.Join(f.root, dir) | ||||
| 	if !strings.HasSuffix(dir, "/") { | ||||
| 		endpoint += "/" | ||||
| 	if !strings.HasSuffix(dir, "/") && dir != "" { | ||||
| 		dir += "/" | ||||
| 	} | ||||
| 	ok, err := f.dirExists(endpoint) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "List failed") | ||||
| 	} | ||||
| 	if !ok { | ||||
| 		return nil, fs.ErrorDirNotFound | ||||
| 	} | ||||
| 	httpDir := path.Join(f.root, dir) | ||||
| 	if !strings.HasSuffix(dir, "/") { | ||||
| 		httpDir += "/" | ||||
| 	} | ||||
| 	infos, err := f.readDir(httpDir) | ||||
| 	names, err := f.readDir(dir) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrapf(err, "error listing %q", dir) | ||||
| 	} | ||||
| 	for _, info := range infos { | ||||
| 		remote := "" | ||||
| 		if dir != "" { | ||||
| 			remote = dir + "/" + info.Name() | ||||
| 		} else { | ||||
| 			remote = info.Name() | ||||
| 		} | ||||
| 		if info.IsDir() { | ||||
| 	for _, name := range names { | ||||
| 		isDir := name[len(name)-1] == '/' | ||||
| 		name = strings.TrimRight(name, "/") | ||||
| 		remote := path.Join(dir, name) | ||||
| 		if isDir { | ||||
| 			dir := &fs.Dir{ | ||||
| 				Name:  remote, | ||||
| 				When:  info.ModTime(), | ||||
| 				When:  timeUnset, | ||||
| 				Bytes: 0, | ||||
| 				Count: 0, | ||||
| 			} | ||||
| @@ -326,7 +318,6 @@ func (f *Fs) List(dir string) (entries fs.DirEntries, err error) { | ||||
| 			file := &Object{ | ||||
| 				fs:     f, | ||||
| 				remote: remote, | ||||
| 				info:   info, | ||||
| 			} | ||||
| 			if err = file.stat(); err != nil { | ||||
| 				continue | ||||
| @@ -371,12 +362,12 @@ func (o *Object) Hash(r fs.HashType) (string, error) { | ||||
|  | ||||
| // Size returns the size in bytes of the remote http file | ||||
| func (o *Object) Size() int64 { | ||||
| 	return o.info.Size() | ||||
| 	return o.size | ||||
| } | ||||
|  | ||||
| // ModTime returns the modification time of the remote http file | ||||
| func (o *Object) ModTime() time.Time { | ||||
| 	return o.info.ModTime() | ||||
| 	return o.modTime | ||||
| } | ||||
|  | ||||
| // path returns the native path of the object | ||||
| @@ -386,37 +377,19 @@ func (o *Object) path() string { | ||||
|  | ||||
| // stat updates the info field in the Object | ||||
| func (o *Object) stat() error { | ||||
| 	endpoint := urlJoin(o.fs.endpoint, o.fs.root, o.remote) | ||||
| 	if o.info.IsDir() { | ||||
| 		endpoint += "/" | ||||
| 	} | ||||
| 	endpoint := urlJoin(o.fs.endpoint, o.remote).String() | ||||
| 	res, err := o.fs.httpClient.Head(endpoint) | ||||
| 	err = statusError(res, err) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return errors.Wrap(err, "failed to stat") | ||||
| 	} | ||||
| 	if res.StatusCode != http.StatusOK { | ||||
| 		return errors.New("failed to stat") | ||||
| 	} | ||||
| 	var mtime int64 | ||||
| 	t, err := http.ParseTime(res.Header.Get("Last-Modified")) | ||||
| 	if err != nil { | ||||
| 		mtime = 0 | ||||
| 	} else { | ||||
| 		mtime = t.Unix() | ||||
| 		t = timeUnset | ||||
| 	} | ||||
| 	size := parseInt64(res.Header.Get("Content-Length")) | ||||
| 	e := &entry{ | ||||
| 		name:  o.remote, | ||||
| 		size:  size, | ||||
| 		mtime: mtime, | ||||
| 		mode:  os.FileMode(0444), | ||||
| 	} | ||||
| 	if strings.HasSuffix(o.remote, "/") { | ||||
| 		e.mode = os.FileMode(0555) | os.ModeDir | ||||
| 		e.size = 0 | ||||
| 		e.name = o.remote[:len(o.remote)-1] | ||||
| 	} | ||||
| 	o.info = e | ||||
| 	o.size = parseInt64(res.Header.Get("Content-Length")) | ||||
| 	o.modTime = t | ||||
| 	o.contentType = res.Header.Get("Content-Type") | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @@ -429,52 +402,29 @@ func (o *Object) SetModTime(modTime time.Time) error { | ||||
|  | ||||
| // Storable returns whether the remote http file is a regular file (not a directory, symbolic link, block device, character device, named pipe, etc) | ||||
| func (o *Object) Storable() bool { | ||||
| 	return o.info.Mode().IsRegular() | ||||
| } | ||||
|  | ||||
| // Read from a remote http file object reader | ||||
| func (file *ObjectReader) Read(p []byte) (n int, err error) { | ||||
| 	n, err = file.httpFile.Read(p) | ||||
| 	return n, err | ||||
| } | ||||
|  | ||||
| // Close a reader of a remote http file | ||||
| func (file *ObjectReader) Close() (err error) { | ||||
| 	return file.httpFile.Close() | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // Open a remote http file object for reading. Seek is supported | ||||
| func (o *Object) Open(options ...fs.OpenOption) (in io.ReadCloser, err error) { | ||||
| 	var offset int64 | ||||
| 	endpoint := urlJoin(o.fs.endpoint, o.fs.root, o.remote) | ||||
| 	offset = 0 | ||||
| 	for _, option := range options { | ||||
| 		switch x := option.(type) { | ||||
| 		case *fs.SeekOption: | ||||
| 			offset = x.Offset | ||||
| 		default: | ||||
| 			if option.Mandatory() { | ||||
| 				fs.Logf(o, "Unsupported mandatory option: %v", option) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	endpoint := urlJoin(o.fs.endpoint, o.remote).String() | ||||
| 	req, err := http.NewRequest("GET", endpoint, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "Open failed") | ||||
| 	} | ||||
| 	if offset > 0 { | ||||
| 		req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset)) | ||||
|  | ||||
| 	// Add optional headers | ||||
| 	for k, v := range fs.OpenOptionHeaders(options) { | ||||
| 		req.Header.Add(k, v) | ||||
| 	} | ||||
|  | ||||
| 	// Do the request | ||||
| 	res, err := o.fs.httpClient.Do(req) | ||||
| 	err = statusError(res, err) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "Open failed") | ||||
| 	} | ||||
| 	in = &ObjectReader{ | ||||
| 		object:   o, | ||||
| 		httpFile: res.Body, | ||||
| 	} | ||||
| 	return in, nil | ||||
| 	return res.Body, nil | ||||
| } | ||||
|  | ||||
| // Hashes returns fs.HashNone to indicate remote hashing is unavailable | ||||
| @@ -502,8 +452,14 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOptio | ||||
| 	return errorReadOnly | ||||
| } | ||||
|  | ||||
| // MimeType of an Object if known, "" otherwise | ||||
| func (o *Object) MimeType() string { | ||||
| 	return o.contentType | ||||
| } | ||||
|  | ||||
| // Check the interfaces are satisfied | ||||
| var ( | ||||
| 	_ fs.Fs     = &Fs{} | ||||
| 	_ fs.Object = &Object{} | ||||
| 	_ fs.Fs        = &Fs{} | ||||
| 	_ fs.Object    = &Object{} | ||||
| 	_ fs.MimeTyper = &Object{} | ||||
| ) | ||||
|   | ||||
							
								
								
									
										308
									
								
								http/http_internal_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								http/http_internal_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,308 @@ | ||||
| // +build go1.7 | ||||
|  | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"sort" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/ncw/rclone/fs" | ||||
| 	"github.com/ncw/rclone/fstest" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	remoteName = "TestHTTP" | ||||
| 	testPath   = "test" | ||||
| 	filesPath  = filepath.Join(testPath, "files") | ||||
| ) | ||||
|  | ||||
| // prepareServer the test server and return a function to tidy it up afterwards | ||||
| func prepareServer(t *testing.T) func() { | ||||
| 	// file server for test/files | ||||
| 	fileServer := http.FileServer(http.Dir(filesPath)) | ||||
|  | ||||
| 	// Make the test server | ||||
| 	ts := httptest.NewServer(fileServer) | ||||
|  | ||||
| 	// Configure the remote | ||||
| 	fs.LoadConfig() | ||||
| 	// fs.Config.LogLevel = fs.LogLevelDebug | ||||
| 	// fs.Config.DumpHeaders = true | ||||
| 	// fs.Config.DumpBodies = true | ||||
| 	fs.ConfigFileSet(remoteName, "type", "http") | ||||
| 	fs.ConfigFileSet(remoteName, "url", ts.URL) | ||||
|  | ||||
| 	// return a function to tidy up | ||||
| 	return ts.Close | ||||
| } | ||||
|  | ||||
| // prepare the test server and return a function to tidy it up afterwards | ||||
| func prepare(t *testing.T) (fs.Fs, func()) { | ||||
| 	tidy := prepareServer(t) | ||||
|  | ||||
| 	// Instantiate it | ||||
| 	f, err := NewFs(remoteName, "") | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	return f, tidy | ||||
| } | ||||
|  | ||||
| func testListRoot(t *testing.T, f fs.Fs) { | ||||
| 	entries, err := f.List("") | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	sort.Sort(entries) | ||||
|  | ||||
| 	require.Equal(t, 4, len(entries)) | ||||
|  | ||||
| 	e := entries[0] | ||||
| 	assert.Equal(t, "four", e.Remote()) | ||||
| 	assert.Equal(t, int64(0), e.Size()) | ||||
| 	_, ok := e.(*fs.Dir) | ||||
| 	assert.True(t, ok) | ||||
|  | ||||
| 	e = entries[1] | ||||
| 	assert.Equal(t, "one.txt", e.Remote()) | ||||
| 	assert.Equal(t, int64(6), e.Size()) | ||||
| 	_, ok = e.(*Object) | ||||
| 	assert.True(t, ok) | ||||
|  | ||||
| 	e = entries[2] | ||||
| 	assert.Equal(t, "three", e.Remote()) | ||||
| 	assert.Equal(t, int64(0), e.Size()) | ||||
| 	_, ok = e.(*fs.Dir) | ||||
| 	assert.True(t, ok) | ||||
|  | ||||
| 	e = entries[3] | ||||
| 	assert.Equal(t, "two.html", e.Remote()) | ||||
| 	assert.Equal(t, int64(7), e.Size()) | ||||
| 	_, ok = e.(*Object) | ||||
| 	assert.True(t, ok) | ||||
| } | ||||
|  | ||||
| func TestListRoot(t *testing.T) { | ||||
| 	f, tidy := prepare(t) | ||||
| 	defer tidy() | ||||
| 	testListRoot(t, f) | ||||
| } | ||||
|  | ||||
| func TestListSubDir(t *testing.T) { | ||||
| 	f, tidy := prepare(t) | ||||
| 	defer tidy() | ||||
|  | ||||
| 	entries, err := f.List("three") | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	sort.Sort(entries) | ||||
|  | ||||
| 	assert.Equal(t, 1, len(entries)) | ||||
|  | ||||
| 	e := entries[0] | ||||
| 	assert.Equal(t, "three/underthree.txt", e.Remote()) | ||||
| 	assert.Equal(t, int64(9), e.Size()) | ||||
| 	_, ok := e.(*Object) | ||||
| 	assert.True(t, ok) | ||||
| } | ||||
|  | ||||
| func TestNewObject(t *testing.T) { | ||||
| 	f, tidy := prepare(t) | ||||
| 	defer tidy() | ||||
|  | ||||
| 	o, err := f.NewObject("four/underfour.txt") | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	assert.Equal(t, "four/underfour.txt", o.Remote()) | ||||
| 	assert.Equal(t, int64(9), o.Size()) | ||||
| 	_, ok := o.(*Object) | ||||
| 	assert.True(t, ok) | ||||
|  | ||||
| 	// Test the time is correct on the object | ||||
|  | ||||
| 	tObj := o.ModTime() | ||||
|  | ||||
| 	fi, err := os.Stat(filepath.Join(filesPath, "four", "underfour.txt")) | ||||
| 	require.NoError(t, err) | ||||
| 	tFile := fi.ModTime() | ||||
|  | ||||
| 	dt, ok := fstest.CheckTimeEqualWithPrecision(tObj, tFile, time.Second) | ||||
| 	assert.True(t, ok, fmt.Sprintf("%s: Modification time difference too big |%s| > %s (%s vs %s) (precision %s)", o.Remote(), dt, time.Second, tObj, tFile, time.Second)) | ||||
| } | ||||
|  | ||||
| func TestOpen(t *testing.T) { | ||||
| 	f, tidy := prepare(t) | ||||
| 	defer tidy() | ||||
|  | ||||
| 	o, err := f.NewObject("four/underfour.txt") | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	// Test normal read | ||||
| 	fd, err := o.Open() | ||||
| 	require.NoError(t, err) | ||||
| 	data, err := ioutil.ReadAll(fd) | ||||
| 	require.NoError(t, fd.Close()) | ||||
| 	assert.Equal(t, "beetroot\n", string(data)) | ||||
|  | ||||
| 	// Test with range request | ||||
| 	fd, err = o.Open(&fs.RangeOption{Start: 1, End: 5}) | ||||
| 	require.NoError(t, err) | ||||
| 	data, err = ioutil.ReadAll(fd) | ||||
| 	require.NoError(t, fd.Close()) | ||||
| 	assert.Equal(t, "eetro", string(data)) | ||||
| } | ||||
|  | ||||
| func TestMimeType(t *testing.T) { | ||||
| 	f, tidy := prepare(t) | ||||
| 	defer tidy() | ||||
|  | ||||
| 	o, err := f.NewObject("four/underfour.txt") | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	do, ok := o.(fs.MimeTyper) | ||||
| 	require.True(t, ok) | ||||
| 	assert.Equal(t, "text/plain; charset=utf-8", do.MimeType()) | ||||
| } | ||||
|  | ||||
| func TestIsAFileRoot(t *testing.T) { | ||||
| 	tidy := prepareServer(t) | ||||
| 	defer tidy() | ||||
|  | ||||
| 	f, err := NewFs(remoteName, "one.txt") | ||||
| 	assert.Equal(t, err, fs.ErrorIsFile) | ||||
|  | ||||
| 	testListRoot(t, f) | ||||
| } | ||||
|  | ||||
| func TestIsAFileSubDir(t *testing.T) { | ||||
| 	tidy := prepareServer(t) | ||||
| 	defer tidy() | ||||
|  | ||||
| 	f, err := NewFs(remoteName, "three/underthree.txt") | ||||
| 	assert.Equal(t, err, fs.ErrorIsFile) | ||||
|  | ||||
| 	entries, err := f.List("") | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	sort.Sort(entries) | ||||
|  | ||||
| 	assert.Equal(t, 1, len(entries)) | ||||
|  | ||||
| 	e := entries[0] | ||||
| 	assert.Equal(t, "underthree.txt", e.Remote()) | ||||
| 	assert.Equal(t, int64(9), e.Size()) | ||||
| 	_, ok := e.(*Object) | ||||
| 	assert.True(t, ok) | ||||
| } | ||||
|  | ||||
| func TestParseName(t *testing.T) { | ||||
| 	for i, test := range []struct { | ||||
| 		base   string | ||||
| 		val    string | ||||
| 		wantOK bool | ||||
| 		want   string | ||||
| 	}{ | ||||
| 		{"http://example.com/", "potato", true, "potato"}, | ||||
| 		{"http://example.com/dir/", "potato", true, "potato"}, | ||||
| 		{"http://example.com/dir/", "../dir/potato", true, "potato"}, | ||||
| 		{"http://example.com/dir/", "..", false, ""}, | ||||
| 		{"http://example.com/dir/", "http://example.com/", false, ""}, | ||||
| 		{"http://example.com/dir/", "http://example.com/dir/", false, ""}, | ||||
| 		{"http://example.com/dir/", "http://example.com/dir/potato", true, "potato"}, | ||||
| 		{"http://example.com/dir/", "/dir/", false, ""}, | ||||
| 		{"http://example.com/dir/", "/dir/potato", true, "potato"}, | ||||
| 		{"http://example.com/dir/", "subdir/potato", false, ""}, | ||||
| 	} { | ||||
| 		u, err := url.Parse(test.base) | ||||
| 		require.NoError(t, err) | ||||
| 		got, gotOK := parseName(u, test.val) | ||||
| 		what := fmt.Sprintf("test %d base=%q, val=%q", i, test.base, test.val) | ||||
| 		assert.Equal(t, test.wantOK, gotOK, what) | ||||
| 		assert.Equal(t, test.want, got, what) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Load HTML from the file given and parse it, checking it against the entries passed in | ||||
| func parseHTML(t *testing.T, name string, base string, want []string) { | ||||
| 	in, err := os.Open(filepath.Join(testPath, "index_files", name)) | ||||
| 	require.NoError(t, err) | ||||
| 	defer func() { | ||||
| 		require.NoError(t, in.Close()) | ||||
| 	}() | ||||
| 	if base == "" { | ||||
| 		base = "http://example.com/" | ||||
| 	} | ||||
| 	u, err := url.Parse(base) | ||||
| 	require.NoError(t, err) | ||||
| 	entries, err := parse(u, in) | ||||
| 	require.NoError(t, err) | ||||
| 	assert.Equal(t, want, entries) | ||||
| } | ||||
|  | ||||
| func TestParseEmpty(t *testing.T) { | ||||
| 	parseHTML(t, "empty.html", "", []string(nil)) | ||||
| } | ||||
|  | ||||
| func TestParseApache(t *testing.T) { | ||||
| 	parseHTML(t, "apache.html", "http://example.com/nick/pub/", []string{ | ||||
| 		"SWIG-embed.tar.gz", | ||||
| 		"avi2dvd.pl", | ||||
| 		"cambert.exe", | ||||
| 		"cambert.gz", | ||||
| 		"fedora_demo.gz", | ||||
| 		"gchq-challenge/", | ||||
| 		"mandelterm/", | ||||
| 		"pgp-key.txt", | ||||
| 		"pymath/", | ||||
| 		"rclone", | ||||
| 		"readdir.exe", | ||||
| 		"rush_hour_solver_cut_down.py", | ||||
| 		"snake-puzzle/", | ||||
| 		"stressdisk/", | ||||
| 		"timer-test", | ||||
| 		"words-to-regexp.pl", | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestParseMemstore(t *testing.T) { | ||||
| 	parseHTML(t, "memstore.html", "", []string{ | ||||
| 		"test/", | ||||
| 		"v1.35/", | ||||
| 		"v1.36-01-g503cd84/", | ||||
| 		"rclone-beta-latest-freebsd-386.zip", | ||||
| 		"rclone-beta-latest-freebsd-amd64.zip", | ||||
| 		"rclone-beta-latest-windows-amd64.zip", | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestParseNginx(t *testing.T) { | ||||
| 	parseHTML(t, "nginx.html", "", []string{ | ||||
| 		"deltas/", | ||||
| 		"objects/", | ||||
| 		"refs/", | ||||
| 		"state/", | ||||
| 		"config", | ||||
| 		"summary", | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestParseCaddy(t *testing.T) { | ||||
| 	parseHTML(t, "caddy.html", "", []string{ | ||||
| 		"mimetype.zip", | ||||
| 		"rclone-delete-empty-dirs.py", | ||||
| 		"rclone-show-empty-dirs.py", | ||||
| 		"stat-windows-386.zip", | ||||
| 		"v1.36-155-gcf29ee8b-team-driveβ/", | ||||
| 		"v1.36-156-gca76b3fb-team-driveβ/", | ||||
| 		"v1.36-156-ge1f0e0f5-team-driveβ/", | ||||
| 		"v1.36-22-g06ea13a-ssh-agentβ/", | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										6
									
								
								http/http_unsupported.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								http/http_unsupported.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| // Build for mount for unsupported platforms to stop go complaining | ||||
| // about "no buildable Go source files " | ||||
|  | ||||
| // +build !go1.7 | ||||
|  | ||||
| package http | ||||
							
								
								
									
										1
									
								
								http/test/files/four/underfour.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								http/test/files/four/underfour.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| beetroot | ||||
							
								
								
									
										1
									
								
								http/test/files/one.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								http/test/files/one.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| hello | ||||
							
								
								
									
										1
									
								
								http/test/files/three/underthree.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								http/test/files/three/underthree.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| rutabaga | ||||
							
								
								
									
										1
									
								
								http/test/files/two.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								http/test/files/two.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| potato | ||||
							
								
								
									
										28
									
								
								http/test/index_files/apache.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								http/test/index_files/apache.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> | ||||
| <html> | ||||
|  <head> | ||||
|   <title>Index of /nick/pub</title> | ||||
|  </head> | ||||
|  <body> | ||||
| <h1>Index of /nick/pub</h1> | ||||
| <table><tr><th><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr><tr><th colspan="5"><hr></th></tr> | ||||
| <tr><td valign="top"><img src="/icons/back.gif" alt="[DIR]"></td><td><a href="/nick/">Parent Directory</a></td><td> </td><td align="right">  - </td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/compressed.gif" alt="[   ]"></td><td><a href="SWIG-embed.tar.gz">SWIG-embed.tar.gz</a></td><td align="right">29-Nov-2005 16:27  </td><td align="right">2.3K</td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/text.gif" alt="[TXT]"></td><td><a href="avi2dvd.pl">avi2dvd.pl</a></td><td align="right">14-Apr-2010 23:07  </td><td align="right"> 17K</td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/binary.gif" alt="[   ]"></td><td><a href="cambert.exe">cambert.exe</a></td><td align="right">15-Dec-2006 18:07  </td><td align="right"> 54K</td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/compressed.gif" alt="[   ]"></td><td><a href="cambert.gz">cambert.gz</a></td><td align="right">14-Apr-2010 23:07  </td><td align="right"> 18K</td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/compressed.gif" alt="[   ]"></td><td><a href="fedora_demo.gz">fedora_demo.gz</a></td><td align="right">08-Jun-2007 11:01  </td><td align="right">1.0M</td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="gchq-challenge/">gchq-challenge/</a></td><td align="right">24-Dec-2016 15:24  </td><td align="right">  - </td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="mandelterm/">mandelterm/</a></td><td align="right">13-Jul-2013 22:22  </td><td align="right">  - </td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/text.gif" alt="[TXT]"></td><td><a href="pgp-key.txt">pgp-key.txt</a></td><td align="right">14-Apr-2010 23:07  </td><td align="right">400 </td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="pymath/">pymath/</a></td><td align="right">24-Dec-2016 15:24  </td><td align="right">  - </td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/unknown.gif" alt="[   ]"></td><td><a href="rclone">rclone</a></td><td align="right">09-May-2017 17:15  </td><td align="right"> 22M</td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/binary.gif" alt="[   ]"></td><td><a href="readdir.exe">readdir.exe</a></td><td align="right">21-Oct-2016 14:47  </td><td align="right">1.6M</td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/text.gif" alt="[TXT]"></td><td><a href="rush_hour_solver_cut_down.py">rush_hour_solver_cut_down.py</a></td><td align="right">23-Jul-2009 11:44  </td><td align="right"> 14K</td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="snake-puzzle/">snake-puzzle/</a></td><td align="right">25-Sep-2016 20:56  </td><td align="right">  - </td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="stressdisk/">stressdisk/</a></td><td align="right">08-Nov-2016 14:25  </td><td align="right">  - </td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/unknown.gif" alt="[   ]"></td><td><a href="timer-test">timer-test</a></td><td align="right">09-May-2017 17:05  </td><td align="right">1.5M</td><td> </td></tr> | ||||
| <tr><td valign="top"><img src="/icons/text.gif" alt="[TXT]"></td><td><a href="words-to-regexp.pl">words-to-regexp.pl</a></td><td align="right">01-Mar-2005 20:43  </td><td align="right">6.0K</td><td> </td></tr> | ||||
| <tr><th colspan="5"><hr></th></tr> | ||||
| </table> | ||||
| </body></html> | ||||
							
								
								
									
										378
									
								
								http/test/index_files/caddy.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										378
									
								
								http/test/index_files/caddy.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,378 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| 	<head> | ||||
| 		<title>/</title> | ||||
| 		<meta charset="utf-8"> | ||||
| 		<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
| <style> | ||||
| * { padding: 0; margin: 0; } | ||||
|  | ||||
| body { | ||||
| 	font-family: sans-serif; | ||||
| 	text-rendering: optimizespeed; | ||||
| } | ||||
|  | ||||
| a { | ||||
| 	color: #006ed3; | ||||
| 	text-decoration: none; | ||||
| } | ||||
|  | ||||
| a:hover, | ||||
| h1 a:hover { | ||||
| 	color: #319cff; | ||||
| } | ||||
|  | ||||
| header, | ||||
| #summary { | ||||
| 	padding-left: 5%; | ||||
| 	padding-right: 5%; | ||||
| } | ||||
|  | ||||
| th:first-child, | ||||
| td:first-child { | ||||
| 	padding-left: 5%; | ||||
| } | ||||
|  | ||||
| th:last-child, | ||||
| td:last-child { | ||||
| 	padding-right: 5%; | ||||
| } | ||||
|  | ||||
| header { | ||||
| 	padding-top: 25px; | ||||
| 	padding-bottom: 15px; | ||||
| 	background-color: #f2f2f2; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
| 	font-size: 20px; | ||||
| 	font-weight: normal; | ||||
| 	white-space: nowrap; | ||||
| 	overflow-x: hidden; | ||||
| 	text-overflow: ellipsis; | ||||
| } | ||||
|  | ||||
| h1 a { | ||||
| 	color: inherit; | ||||
| } | ||||
|  | ||||
| h1 a:hover { | ||||
| 	text-decoration: underline; | ||||
| } | ||||
|  | ||||
| main { | ||||
| 	display: block; | ||||
| } | ||||
|  | ||||
| .meta { | ||||
| 	font-size: 12px; | ||||
| 	font-family: Verdana, sans-serif; | ||||
| 	border-bottom: 1px solid #9C9C9C; | ||||
| 	padding-top: 10px; | ||||
| 	padding-bottom: 10px; | ||||
| } | ||||
|  | ||||
| .meta-item { | ||||
| 	margin-right: 1em; | ||||
| } | ||||
|  | ||||
| #filter { | ||||
| 	padding: 4px; | ||||
| 	border: 1px solid #CCC; | ||||
| } | ||||
|  | ||||
| table { | ||||
| 	width: 100%; | ||||
| 	border-collapse: collapse; | ||||
| } | ||||
|  | ||||
| tr { | ||||
| 	border-bottom: 1px dashed #dadada; | ||||
| } | ||||
|  | ||||
| tbody tr:hover { | ||||
| 	background-color: #ffffec; | ||||
| } | ||||
|  | ||||
| th, | ||||
| td { | ||||
| 	text-align: left; | ||||
| 	padding: 10px 0; | ||||
| } | ||||
|  | ||||
| th { | ||||
| 	padding-top: 15px; | ||||
| 	padding-bottom: 15px; | ||||
| 	font-size: 16px; | ||||
| 	white-space: nowrap; | ||||
| } | ||||
|  | ||||
| th a { | ||||
| 	color: black; | ||||
| } | ||||
|  | ||||
| th svg { | ||||
| 	vertical-align: middle; | ||||
| } | ||||
|  | ||||
| td { | ||||
| 	font-size: 14px; | ||||
| } | ||||
|  | ||||
| td:first-child { | ||||
| 	width: 50%; | ||||
| } | ||||
|  | ||||
| th:last-child, | ||||
| td:last-child { | ||||
| 	text-align: right; | ||||
| } | ||||
|  | ||||
| td:first-child svg { | ||||
| 	position: absolute; | ||||
| } | ||||
|  | ||||
| td .name, | ||||
| td .goup { | ||||
| 	margin-left: 1.75em; | ||||
| 	word-break: break-all; | ||||
| 	overflow-wrap: break-word; | ||||
| 	white-space: pre-wrap; | ||||
| } | ||||
|  | ||||
| footer { | ||||
| 	padding: 40px 20px; | ||||
| 	font-size: 12px; | ||||
| 	text-align: center; | ||||
| } | ||||
|  | ||||
| @media (max-width: 600px) { | ||||
| 	.hideable { | ||||
| 		display: none; | ||||
| 	} | ||||
|  | ||||
| 	td:first-child { | ||||
| 		width: auto; | ||||
| 	} | ||||
|  | ||||
| 	th:nth-child(2), | ||||
| 	td:nth-child(2) { | ||||
| 		padding-right: 5%; | ||||
| 		text-align: right; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 		<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="0" width="0" style="position: absolute;"> | ||||
| 			<defs> | ||||
| 				<!-- Folder --> | ||||
| 				<linearGradient id="f" y2="640" gradientUnits="userSpaceOnUse" x2="244.84" gradientTransform="matrix(.97319 0 0 1.0135 -.50695 -13.679)" y1="415.75" x1="244.84"> | ||||
| 					<stop stop-color="#b3ddfd" offset="0"/> | ||||
| 					<stop stop-color="#69c" offset="1"/> | ||||
| 				</linearGradient> | ||||
| 				<linearGradient id="e" y2="571.06" gradientUnits="userSpaceOnUse" x2="238.03" gradientTransform="translate(0,2)" y1="346.05" x1="236.26"> | ||||
| 					<stop stop-color="#ace" offset="0"/> | ||||
| 					<stop stop-color="#369" offset="1"/> | ||||
| 				</linearGradient> | ||||
| 				<g id="folder" transform="translate(-266.06 -193.36)"> | ||||
| 					<g transform="matrix(.066019 0 0 .066019 264.2 170.93)"> | ||||
| 						<g transform="matrix(1.4738 0 0 1.4738 -52.053 -166.93)"> | ||||
| 							<path fill="#69c" d="m98.424 343.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/> | ||||
| 							<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="409.69" x="54.428" fill="#369"/> | ||||
| 							<path fill="url(#e)" d="m98.424 345.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/> | ||||
| 							<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="407.69" x="54.428" fill="url(#f)"/> | ||||
| 						</g> | ||||
| 					</g> | ||||
| 				</g> | ||||
|  | ||||
| 				<!-- File --> | ||||
| 				<linearGradient id="a"> | ||||
| 					<stop stop-color="#cbcbcb" offset="0"/> | ||||
| 					<stop stop-color="#f0f0f0" offset=".34923"/> | ||||
| 					<stop stop-color="#e2e2e2" offset="1"/> | ||||
| 				</linearGradient> | ||||
| 				<linearGradient id="d" y2="686.15" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="207.83" gradientTransform="matrix(.28346 0 0 .31053 -608.52 485.11)" x2="380.1" x1="749.25"/> | ||||
| 				<linearGradient id="c" y2="287.74" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="169.44" gradientTransform="matrix(.28342 0 0 .31057 -608.52 485.11)" x2="622.33" x1="741.64"/> | ||||
| 				<linearGradient id="b" y2="418.54" gradientUnits="userSpaceOnUse" y1="236.13" gradientTransform="matrix(.29343 0 0 .29999 -608.52 485.11)" x2="330.88" x1="687.96"> | ||||
| 					<stop stop-color="#fff" offset="0"/> | ||||
| 					<stop stop-color="#fff" stop-opacity="0" offset="1"/> | ||||
| 				</linearGradient> | ||||
| 				<g id="file" transform="translate(-278.15 -216.59)"> | ||||
| 					<g fill-rule="evenodd" transform="matrix(.19775 0 0 .19775 381.05 112.68)"> | ||||
| 						<path d="m-520.17 525.5v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke-width=".42649" fill="#fff"/> | ||||
| 						<g> | ||||
| 							<path d="m-520.11 525.68v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke="#000" stroke-width=".42649" fill="url(#d)"/> | ||||
| 							<path d="m-386 562.42c-10.108-2.9925-23.206-2.5682-33.101-0.86253 1.7084-10.962 1.922-24.701-0.4271-35.877l33.528 36.739z" stroke-width=".95407pt" fill="url(#c)"/> | ||||
| 							<path d="m-519.13 537-0.60402 134.7h131.68l0.0755-33.296c-2.9446 1.1325-32.692-40.998-70.141-39.186-37.483 1.8137-27.785-56.777-61.006-62.214z" stroke-width="1pt" fill="url(#b)"/> | ||||
| 						</g> | ||||
| 					</g> | ||||
| 				</g> | ||||
|  | ||||
| 				<!-- Up arrow --> | ||||
| 				<g id="up-arrow" transform="translate(-279.22 -208.12)"> | ||||
| 					<path transform="matrix(.22413 0 0 .12089 335.67 164.35)" stroke-width="0" d="m-194.17 412.01h-28.827-28.827l14.414-24.965 14.414-24.965 14.414 24.965z"/> | ||||
| 				</g> | ||||
|  | ||||
| 				<!-- Down arrow --> | ||||
| 				<g id="down-arrow" transform="translate(-279.22 -208.12)"> | ||||
| 					<path transform="matrix(.22413 0 0 -.12089 335.67 257.93)" stroke-width="0" d="m-194.17 412.01h-28.827-28.827l14.414-24.965 14.414-24.965 14.414 24.965z"/> | ||||
| 				</g> | ||||
| 			</defs> | ||||
| 		</svg> | ||||
|  | ||||
| 		<header> | ||||
| 			<h1> | ||||
| 				<a href="/">/</a> | ||||
| 			</h1> | ||||
| 		</header> | ||||
| 		<main> | ||||
| 			<div class="meta"> | ||||
| 				<div id="summary"> | ||||
| 					<span class="meta-item"><b>4</b> directories</span> | ||||
| 					<span class="meta-item"><b>4</b> files</span> | ||||
| 					<span class="meta-item"><input type="text" placeholder="filter" id="filter" onkeyup='filter()'></span> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="listing"> | ||||
| 				<table aria-describedby="summary"> | ||||
| 					<thead> | ||||
| 					<tr> | ||||
| 						<th> | ||||
| 							<a href="?sort=name&order=desc">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a> | ||||
| 						</th> | ||||
| 						<th> | ||||
| 							<a href="?sort=size&order=asc">Size</a> | ||||
| 						</th> | ||||
| 						<th class="hideable"> | ||||
| 							<a href="?sort=time&order=asc">Modified</a> | ||||
| 						</th> | ||||
| 					</tr> | ||||
| 					</thead> | ||||
| 					<tbody> | ||||
| 					<tr class="file"> | ||||
| 						<td> | ||||
| 							<a href="./mimetype.zip"> | ||||
| 								<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 26.604381 29.144726"><use xlink:href="#file"></use></svg> | ||||
| 								<span class="name">mimetype.zip</span> | ||||
| 							</a> | ||||
| 						</td> | ||||
| 						<td data-order="783696">765 KiB</td> | ||||
| 						<td class="hideable"><time datetime="2016-04-04T15:36:49Z">04/04/2016 03:36:49 PM +00:00</time></td> | ||||
| 					</tr> | ||||
| 					<tr class="file"> | ||||
| 						<td> | ||||
| 							<a href="./rclone-delete-empty-dirs.py"> | ||||
| 								<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 26.604381 29.144726"><use xlink:href="#file"></use></svg> | ||||
| 								<span class="name">rclone-delete-empty-dirs.py</span> | ||||
| 							</a> | ||||
| 						</td> | ||||
| 						<td data-order="1271">1.2 KiB</td> | ||||
| 						<td class="hideable"><time datetime="2016-10-26T16:05:08Z">10/26/2016 04:05:08 PM +00:00</time></td> | ||||
| 					</tr> | ||||
| 					<tr class="file"> | ||||
| 						<td> | ||||
| 							<a href="./rclone-show-empty-dirs.py"> | ||||
| 								<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 26.604381 29.144726"><use xlink:href="#file"></use></svg> | ||||
| 								<span class="name">rclone-show-empty-dirs.py</span> | ||||
| 							</a> | ||||
| 						</td> | ||||
| 						<td data-order="868">868 B</td> | ||||
| 						<td class="hideable"><time datetime="2016-10-26T09:29:34Z">10/26/2016 09:29:34 AM +00:00</time></td> | ||||
| 					</tr> | ||||
| 					<tr class="file"> | ||||
| 						<td> | ||||
| 							<a href="./stat-windows-386.zip"> | ||||
| 								<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 26.604381 29.144726"><use xlink:href="#file"></use></svg> | ||||
| 								<span class="name">stat-windows-386.zip</span> | ||||
| 							</a> | ||||
| 						</td> | ||||
| 						<td data-order="704960">688 KiB</td> | ||||
| 						<td class="hideable"><time datetime="2016-08-14T20:44:58Z">08/14/2016 08:44:58 PM +00:00</time></td> | ||||
| 					</tr> | ||||
| 					<tr class="file"> | ||||
| 						<td> | ||||
| 							<a href="./v1.36-155-gcf29ee8b-team-drive%CE%B2/"> | ||||
| 								<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 35.678803 28.527945"><use xlink:href="#folder"></use></svg> | ||||
| 								<span class="name">v1.36-155-gcf29ee8b-team-driveβ</span> | ||||
| 							</a> | ||||
| 						</td> | ||||
| 						<td data-order="-1">—</td> | ||||
| 						<td class="hideable"><time datetime="2017-06-01T21:28:09Z">06/01/2017 09:28:09 PM +00:00</time></td> | ||||
| 					</tr> | ||||
| 					<tr class="file"> | ||||
| 						<td> | ||||
| 							<a href="./v1.36-156-gca76b3fb-team-drive%CE%B2/"> | ||||
| 								<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 35.678803 28.527945"><use xlink:href="#folder"></use></svg> | ||||
| 								<span class="name">v1.36-156-gca76b3fb-team-driveβ</span> | ||||
| 							</a> | ||||
| 						</td> | ||||
| 						<td data-order="-1">—</td> | ||||
| 						<td class="hideable"><time datetime="2017-06-04T08:53:04Z">06/04/2017 08:53:04 AM +00:00</time></td> | ||||
| 					</tr> | ||||
| 					<tr class="file"> | ||||
| 						<td> | ||||
| 							<a href="./v1.36-156-ge1f0e0f5-team-drive%CE%B2/"> | ||||
| 								<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 35.678803 28.527945"><use xlink:href="#folder"></use></svg> | ||||
| 								<span class="name">v1.36-156-ge1f0e0f5-team-driveβ</span> | ||||
| 							</a> | ||||
| 						</td> | ||||
| 						<td data-order="-1">—</td> | ||||
| 						<td class="hideable"><time datetime="2017-06-02T10:38:05Z">06/02/2017 10:38:05 AM +00:00</time></td> | ||||
| 					</tr> | ||||
| 					<tr class="file"> | ||||
| 						<td> | ||||
| 							<a href="./v1.36-22-g06ea13a-ssh-agent%CE%B2/"> | ||||
| 								<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 35.678803 28.527945"><use xlink:href="#folder"></use></svg> | ||||
| 								<span class="name">v1.36-22-g06ea13a-ssh-agentβ</span> | ||||
| 							</a> | ||||
| 						</td> | ||||
| 						<td data-order="-1">—</td> | ||||
| 						<td class="hideable"><time datetime="2017-04-10T13:58:02Z">04/10/2017 01:58:02 PM +00:00</time></td> | ||||
| 					</tr> | ||||
| 					</tbody> | ||||
| 				</table> | ||||
| 			</div> | ||||
| 		</main> | ||||
| 		<footer> | ||||
| 			Served with <a rel="noopener noreferrer" href="https://caddyserver.com">Caddy</a> | ||||
| 		</footer> | ||||
| 		<script> | ||||
| 			var filterEl = document.getElementById('filter'); | ||||
| 			function filter() { | ||||
| 				var q = filterEl.value.trim().toLowerCase(); | ||||
| 				var elems = document.querySelectorAll('tr.file'); | ||||
| 				elems.forEach(function(el) { | ||||
| 					if (!q) { | ||||
| 						el.style.display = ''; | ||||
| 						return; | ||||
| 					} | ||||
| 					var nameEl = el.querySelector('.name'); | ||||
| 					var nameVal = nameEl.textContent.trim().toLowerCase(); | ||||
| 					if (nameVal.indexOf(q) !== -1) { | ||||
| 						el.style.display = ''; | ||||
| 					} else { | ||||
| 						el.style.display = 'none'; | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
|  | ||||
| 			function localizeDatetime(e, index, ar) { | ||||
| 				if (e.textContent === undefined) { | ||||
| 					return; | ||||
| 				} | ||||
| 				var d = new Date(e.getAttribute('datetime')); | ||||
| 				if (isNaN(d)) { | ||||
| 					d = new Date(e.textContent); | ||||
| 					if (isNaN(d)) { | ||||
| 						return; | ||||
| 					} | ||||
| 				} | ||||
| 				e.textContent = d.toLocaleString(); | ||||
| 			} | ||||
| 			var timeList = Array.prototype.slice.call(document.getElementsByTagName("time")); | ||||
| 			timeList.forEach(localizeDatetime); | ||||
| 		</script> | ||||
| 	</body> | ||||
| </html> | ||||
|  | ||||
							
								
								
									
										0
									
								
								http/test/index_files/empty.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								http/test/index_files/empty.html
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										77
									
								
								http/test/index_files/memstore.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								http/test/index_files/memstore.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | ||||
| <html xmlns="http://www.w3.org/1999/xhtml"> | ||||
| <head> | ||||
|   <meta http-equiv="content-type" content="text/html; charset=utf-8" /> | ||||
|   <meta name="robots" content="noindex" /> | ||||
|   <title>Index of /</title> | ||||
| </head> | ||||
| <body> | ||||
| <div id="content"> | ||||
|   <h1>Index of /</h1> | ||||
|  | ||||
|   <table> | ||||
|   <thead> | ||||
|       <tr> | ||||
|         <th>Name</th> | ||||
|         <th>Type</th> | ||||
|         <th>Size</th> | ||||
|         <th>Last modified</th> | ||||
|         <th>MD5</th> | ||||
|       </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|    | ||||
|       <tr> | ||||
|         <td><a href="test/">test/</a></td> | ||||
|         <td>application/directory</td> | ||||
|         <td>0 bytes</td> | ||||
|         <td>-</td> | ||||
|         <td>-</td> | ||||
|       </tr> | ||||
|    | ||||
|       <tr> | ||||
|         <td><a href="v1.35/">v1.35/</a></td> | ||||
|         <td>application/directory</td> | ||||
|         <td>0 bytes</td> | ||||
|         <td>-</td> | ||||
|         <td>-</td> | ||||
|       </tr> | ||||
|    | ||||
|       <tr> | ||||
|         <td><a href="v1.36-01-g503cd84/">v1.36-01-g503cd84/</a></td> | ||||
|         <td>application/directory</td> | ||||
|         <td>0 bytes</td> | ||||
|         <td>-</td> | ||||
|         <td>-</td> | ||||
|       </tr> | ||||
|  | ||||
|       <tr> | ||||
|         <td><a href="rclone-beta-latest-freebsd-386.zip">rclone-beta-latest-freebsd-386.zip</a></td> | ||||
|         <td>application/zip</td> | ||||
|         <td>4.6 MB</td> | ||||
|         <td>2017-06-19 14:04:52</td> | ||||
|         <td>e747003c69c81e675f206a715264bfa8</td> | ||||
|       </tr> | ||||
|    | ||||
|       <tr> | ||||
|         <td><a href="rclone-beta-latest-freebsd-amd64.zip">rclone-beta-latest-freebsd-amd64.zip</a></td> | ||||
|         <td>application/zip</td> | ||||
|         <td>5.0 MB</td> | ||||
|         <td>2017-06-19 14:04:53</td> | ||||
|         <td>ff30b5e9bf2863a2373069142e6f2b7f</td> | ||||
|       </tr> | ||||
|    | ||||
|       <tr> | ||||
|         <td><a href="rclone-beta-latest-windows-amd64.zip">rclone-beta-latest-windows-amd64.zip</a></td> | ||||
|         <td>application/x-zip-compressed</td> | ||||
|         <td>4.9 MB</td> | ||||
|         <td>2017-06-19 13:56:02</td> | ||||
|         <td>851a5547a0495cbbd94cbc90a80ed6f5</td> | ||||
|       </tr> | ||||
|    | ||||
|   </tbody> | ||||
|   </table> | ||||
|   <p class="right"><a href="http://www.memset.com/"><img src="http://www.memset.com/images/Memset_logo_2010.gif" alt="Memset Ltd." /></a></p> | ||||
| </div> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										12
									
								
								http/test/index_files/nginx.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								http/test/index_files/nginx.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| <html> | ||||
| <head><title>Index of /atomic/fedora/</title></head> | ||||
| <body bgcolor="white"> | ||||
| <h1>Index of /atomic/fedora/</h1><hr><pre><a href="../">../</a> | ||||
| <a href="deltas/">deltas/</a>                                            04-May-2017 21:37                   - | ||||
| <a href="objects/">objects/</a>                                           04-May-2017 20:44                   - | ||||
| <a href="refs/">refs/</a>                                              04-May-2017 20:42                   - | ||||
| <a href="state/">state/</a>                                             04-May-2017 21:36                   - | ||||
| <a href="config">config</a>                                             04-May-2017 20:42                 118 | ||||
| <a href="summary">summary</a>                                            04-May-2017 21:36                 806 | ||||
| </pre><hr></body> | ||||
| </html> | ||||
		Reference in New Issue
	
	Block a user