-
Notifications
You must be signed in to change notification settings - Fork 247
Adds local filesystem support #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f1ffa6e
e26f542
e01ad88
e6d4114
0e3d3aa
7590cf7
56ed22a
5551226
fb3c345
1d19e99
a46d719
0ea5368
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| package checkup | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "io/ioutil" | ||
| "os" | ||
| "path/filepath" | ||
| "time" | ||
| ) | ||
|
|
||
| const indexName = "index.json" | ||
|
|
||
| // FS is a way to store checkup results on the local filesystem. | ||
| type FS struct { | ||
| // The path to the directory where check files will be stored. | ||
| Dir string `json:"dir"` | ||
| // The URL corresponding to fs.Dir. | ||
| URL string `json:"url"` | ||
|
|
||
| // Check files older than CheckExpiry will be | ||
| // deleted on calls to Maintain(). If this is | ||
| // the zero value, no old check files will be | ||
| // deleted. | ||
| CheckExpiry time.Duration `json:"check_expiry,omitempty"` | ||
| } | ||
|
|
||
| func (fs FS) readIndex() (map[string]int64, error) { | ||
| index := map[string]int64{} | ||
|
|
||
| f, err := os.Open(filepath.Join(fs.Dir, indexName)) | ||
| if os.IsNotExist(err) { | ||
| return index, nil | ||
| } else if err != nil { | ||
| return nil, err | ||
| } | ||
| defer f.Close() | ||
|
|
||
| err = json.NewDecoder(f).Decode(&index) | ||
| return index, err | ||
| } | ||
|
|
||
| func (fs FS) writeIndex(index map[string]int64) error { | ||
| f, err := os.Create(filepath.Join(fs.Dir, indexName)) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer f.Close() | ||
|
|
||
| return json.NewEncoder(f).Encode(index) | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So is the purpose of the index just so that other files can be intermingled with the check files?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it's to allow the JS client to fetch a list of check files. There is no standard way to list all files in a directory in HTTP.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, nevermind - I got to the bottom of this PR and I see that the javascript loads the index file to know which check files to load. Makes sense.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GitHub really needs to make these pages update live. 👎 |
||
|
|
||
| // Store stores results on filesystem according to the configuration in fs. | ||
| func (fs FS) Store(results []Result) error { | ||
| // Write results to a new file | ||
| name := *GenerateFilename() | ||
| f, err := os.Create(filepath.Join(fs.Dir, name)) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| err = json.NewEncoder(f).Encode(results) | ||
| f.Close() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Read current index file | ||
| index, err := fs.readIndex() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Add new file to index | ||
| index[name] = time.Now().UnixNano() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be the same filename that you used above in os.Create?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The index contains both filenames and their timestamp, which is helpful because we don't need to parse filenames anymore. Since filenames are just informative, I though having a slightly different timestamp wouldn't matter too much...
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, right, thanks for clarifying. |
||
|
|
||
| // Write new index | ||
| return fs.writeIndex(index) | ||
| } | ||
|
|
||
| // Maintain deletes check files that are older than fs.CheckExpiry. | ||
| func (fs FS) Maintain() error { | ||
| if fs.CheckExpiry == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| files, err := ioutil.ReadDir(fs.Dir) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| index, err := fs.readIndex() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| for _, f := range files { | ||
| if f.Name() == indexName { | ||
| continue | ||
| } | ||
|
|
||
| nsec, ok := index[f.Name()] | ||
| if !ok { | ||
| continue | ||
| } | ||
|
|
||
| if time.Since(time.Unix(0, nsec)) > fs.CheckExpiry { | ||
| if err := os.Remove(filepath.Join(fs.Dir, f.Name())); err != nil { | ||
| return err | ||
| } | ||
| delete(index, f.Name()) | ||
| } | ||
| } | ||
|
|
||
| return fs.writeIndex(index) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| package checkup | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "io/ioutil" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
| "time" | ||
| ) | ||
|
|
||
| func TestFS(t *testing.T) { | ||
| results := []Result{{Title: "Testing"}} | ||
| resultsBytes := []byte(`[{"title":"Testing"}]`+"\n") | ||
|
|
||
| dir, err := ioutil.TempDir("", "checkup") | ||
| if err != nil { | ||
| t.Fatalf("Cannot create temporary directory: %v", err) | ||
| } | ||
| defer os.RemoveAll(dir) | ||
|
|
||
| specimen := FS{ | ||
| Dir: dir, | ||
| } | ||
|
|
||
| if err := specimen.Store(results); err != nil { | ||
| t.Fatalf("Expected no error from Store(), got: %v", err) | ||
| } | ||
|
|
||
| // Make sure index has been created | ||
| index, err := specimen.readIndex() | ||
| if err != nil { | ||
| t.Fatalf("Cannot read index: %v", err) | ||
| } | ||
|
|
||
| if len(index) != 1 { | ||
| t.Fatalf("Expected length of index to be 1, but got %v", len(index)) | ||
| } | ||
|
|
||
| var ( | ||
| name string | ||
| nsec int64 | ||
| ) | ||
| for name, nsec = range index {} | ||
|
|
||
| // Make sure index has timestamp of check | ||
| ts := time.Unix(0, nsec) | ||
| if time.Since(ts) > 1*time.Second { | ||
| t.Errorf("Timestamp of check is %s but expected something very recent", ts) | ||
| } | ||
|
|
||
| // Make sure check file bytes are correct | ||
| checkfile := filepath.Join(specimen.Dir, name) | ||
| b, err := ioutil.ReadFile(checkfile) | ||
| if err != nil { | ||
| t.Fatalf("Expected no error reading body, got: %v", err) | ||
| } | ||
| if bytes.Compare(b, resultsBytes) != 0 { | ||
| t.Errorf("Contents of file are wrong\nExpected %s\nGot %s", resultsBytes, b) | ||
| } | ||
|
|
||
| // Make sure check file is not deleted after maintain with CheckExpiry == 0 | ||
| if err := specimen.Maintain(); err != nil { | ||
| t.Fatalf("Expected no error, got %v", err) | ||
| } | ||
| if _, err := os.Stat(checkfile); err != nil { | ||
| t.Fatalf("Expected not error calling Stat() on checkfile, got: %v", err) | ||
| } | ||
|
|
||
| // Make sure checkfile is deleted after maintain with CheckExpiry > 0 | ||
| specimen.CheckExpiry = 1 * time.Nanosecond | ||
| if err := specimen.Maintain(); err != nil { | ||
| t.Fatalf("Expected no error, got %v", err) | ||
| } | ||
| if _, err := os.Stat(checkfile); !os.IsNotExist(err) { | ||
| t.Fatalf("Expected checkfile to be deleted, but Stat() returned error: %v", err) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| /** | ||
|
|
||
| FS Storage Adapter for Checkup.js | ||
|
|
||
| **/ | ||
|
|
||
| var checkup = checkup || {}; | ||
|
|
||
| checkup.storage = (function() { | ||
| var url; | ||
|
|
||
| // getCheckFileList gets the list of check files within | ||
| // the given timeframe (as a unit of nanoseconds) to | ||
| // download. | ||
| function getCheckFileList(timeframe, callback) { | ||
| var after = time.Now() - timeframe; | ||
| checkup.getJSON(url+'/index.json', function(index) { | ||
| var names = []; | ||
| for (var name in index) { | ||
| if (index[name] >= after) { | ||
| names.push(name); | ||
| } | ||
| } | ||
| callback(names); | ||
| }); | ||
| }; | ||
|
|
||
| // setup prepares this storage unit to operate. | ||
| this.setup = function(cfg) { | ||
| url = cfg.url; | ||
| }; | ||
|
|
||
| // getChecksWithin gets all the checks within timeframe as a unit | ||
| // of nanoseconds, and executes callback for each check file. | ||
| this.getChecksWithin = function(timeframe, fileCallback, doneCallback) { | ||
| var checksLoaded = 0, resultsLoaded = 0; | ||
| getCheckFileList(timeframe, function(list) { | ||
| if (list.length == 0 && (typeof doneCallback === 'function')) { | ||
| doneCallback(checksLoaded); | ||
| } else { | ||
| for (var i = 0; i < list.length; i++) { | ||
| checkup.getJSON(url+'/'+list[i], function(filename) { | ||
| return function(json, url) { | ||
| checksLoaded++; | ||
| resultsLoaded += json.length; | ||
| if (typeof fileCallback === 'function') | ||
| fileCallback(json, filename); | ||
| if (checksLoaded >= list.length && (typeof doneCallback === 'function')) | ||
| doneCallback(checksLoaded, resultsLoaded); | ||
| }; | ||
| }(list[i])); | ||
| } | ||
| } | ||
| }); | ||
| }; | ||
|
|
||
| // getNewChecks gets any checks since the timestamp on the file name | ||
| // of the youngest check file that has been downloaded. If no check | ||
| // files have been downloaded, no new check files will be loaded. | ||
| this.getNewChecks = function(fileCallback, doneCallback) { | ||
| if (!checkup.lastCheckTs == null) | ||
| return; | ||
| var timeframe = time.Now() - checkup.lastCheckTs; | ||
| return this.getChecksWithin(timeframe, fileCallback, doneCallback); | ||
| }; | ||
|
|
||
| return this; | ||
| })(); |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a matter of style, I would prefer not to have named returns here, and instead start the function with
index := make(map[string]int64). Even though the function is small, I think the explicit returns are a little easier to follow, especially since you have to make the map anyway.