diff --git a/handlers/guest_links.go b/handlers/guest_links.go index a3fa8617..0d0adc74 100644 --- a/handlers/guest_links.go +++ b/handlers/guest_links.go @@ -67,7 +67,8 @@ func (s Server) guestLinksDelete() http.HandlerFunc { func guestLinkFromRequest(r *http.Request) (picoshare.GuestLink, error) { var payload struct { Label string `json:"label"` - Expiration string `json:"expirationTime"` + UrlExpiration string `json:"urlExpirationTime"` + FileExpiration string `json:"fileLifetime"` MaxFileBytes *uint64 `json:"maxFileBytes"` MaxFileUploads *int `json:"maxFileUploads"` } @@ -82,7 +83,12 @@ func guestLinkFromRequest(r *http.Request) (picoshare.GuestLink, error) { return picoshare.GuestLink{}, err } - expiration, err := parse.Expiration(payload.Expiration) + urlExpiration, err := parse.Expiration(payload.UrlExpiration) + if err != nil { + return picoshare.GuestLink{}, err + } + + fileExpiration, err := parse.FileLifetimeFromString(payload.FileExpiration) if err != nil { return picoshare.GuestLink{}, err } @@ -99,7 +105,8 @@ func guestLinkFromRequest(r *http.Request) (picoshare.GuestLink, error) { return picoshare.GuestLink{ Label: label, - Expires: expiration, + UrlExpires: urlExpiration, + FileLifetime: fileExpiration, MaxFileBytes: maxFileBytes, MaxFileUploads: maxFileUploads, }, nil diff --git a/handlers/guest_links_test.go b/handlers/guest_links_test.go index f9ff7f24..96338c9d 100644 --- a/handlers/guest_links_test.go +++ b/handlers/guest_links_test.go @@ -27,13 +27,15 @@ func TestGuestLinksPostAcceptsValidRequest(t *testing.T) { description: "minimally populated request", payload: `{ "label": null, - "expirationTime":"2030-01-02T03:04:25Z", + "urlExpirationTime":"2030-01-02T03:04:25Z", + "fileLifetime":"876000h0m0s", "maxFileBytes": null, "maxFileUploads": null }`, expected: picoshare.GuestLink{ Label: picoshare.GuestLinkLabel(""), - Expires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + FileLifetime: picoshare.FileLifetimeInfinite, MaxFileBytes: picoshare.GuestUploadUnlimitedFileSize, MaxFileUploads: picoshare.GuestUploadUnlimitedFileUploads, }, @@ -42,13 +44,49 @@ func TestGuestLinksPostAcceptsValidRequest(t *testing.T) { description: "fully populated request", payload: `{ "label": "For my good pal, Maurice", - "expirationTime":"2030-01-02T03:04:25Z", + "urlExpirationTime":"2030-01-02T03:04:25Z", + "fileLifetime":"876000h0m0s", "maxFileBytes": 1048576, "maxFileUploads": 1 }`, expected: picoshare.GuestLink{ Label: picoshare.GuestLinkLabel("For my good pal, Maurice"), - Expires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + FileLifetime: picoshare.FileLifetimeInfinite, + MaxFileBytes: makeGuestUploadMaxFileBytes(1048576), + MaxFileUploads: makeGuestUploadCountLimit(1), + }, + }, + { + description: "guest file expires in 1 day", + payload: `{ + "label": "For my good pal, Maurice", + "urlExpirationTime":"2030-01-02T03:04:25Z", + "fileLifetime":"24h0m0s", + "maxFileBytes": 1048576, + "maxFileUploads": 1 + }`, + expected: picoshare.GuestLink{ + Label: picoshare.GuestLinkLabel("For my good pal, Maurice"), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + FileLifetime: picoshare.NewFileLifetimeInDays(1), + MaxFileBytes: makeGuestUploadMaxFileBytes(1048576), + MaxFileUploads: makeGuestUploadCountLimit(1), + }, + }, + { + description: "guest file expires in 30 day", + payload: `{ + "label": "For my good pal, Maurice", + "urlExpirationTime":"2030-01-02T03:04:25Z", + "fileLifetime":"720h0m0s", + "maxFileBytes": 1048576, + "maxFileUploads": 1 + }`, + expected: picoshare.GuestLink{ + Label: picoshare.GuestLinkLabel("For my good pal, Maurice"), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + FileLifetime: picoshare.NewFileLifetimeInDays(30), MaxFileBytes: makeGuestUploadMaxFileBytes(1048576), MaxFileUploads: makeGuestUploadCountLimit(1), }, @@ -117,7 +155,7 @@ func TestGuestLinksPostRejectsInvalidRequest(t *testing.T) { description: "invalid label field (non-string)", payload: `{ "label": 5, - "expirationTime":"2025-01-01T00:00:00Z", + "urlExpirationTime":"2025-01-01T00:00:00Z", "maxFileBytes": null, "maxFileUploads": null }`, @@ -126,13 +164,13 @@ func TestGuestLinksPostRejectsInvalidRequest(t *testing.T) { description: "invalid label field (too long)", payload: fmt.Sprintf(`{ "label": "%s", - "expirationTime":"2025-01-01T00:00:00Z", + "urlExpirationTime":"2025-01-01T00:00:00Z", "maxFileBytes": null, "maxFileUploads": null }`, strings.Repeat("A", 201)), }, { - description: "missing expirationTime field", + description: "missing urlExpirationTime field", payload: `{ "label": null, "maxFileBytes": null, @@ -143,7 +181,7 @@ func TestGuestLinksPostRejectsInvalidRequest(t *testing.T) { description: "invalid expirationTime field", payload: `{ "label": null, - "expirationTime": 25, + "urlExpirationTime": 25, "maxFileBytes": null, "maxFileUploads": null }`, @@ -152,7 +190,7 @@ func TestGuestLinksPostRejectsInvalidRequest(t *testing.T) { description: "negative maxFileBytes field", payload: `{ "label": null, - "expirationTime":"2025-01-01T00:00:00Z", + "urlExpirationTime":"2025-01-01T00:00:00Z", "maxFileBytes": -5, "maxFileUploads": null }`, @@ -161,7 +199,7 @@ func TestGuestLinksPostRejectsInvalidRequest(t *testing.T) { description: "decimal maxFileBytes field", payload: `{ "label": null, - "expirationTime":"2025-01-01T00:00:00Z", + "urlExpirationTime":"2025-01-01T00:00:00Z", "maxFileBytes": 1.5, "maxFileUploads": null }`, @@ -170,7 +208,7 @@ func TestGuestLinksPostRejectsInvalidRequest(t *testing.T) { description: "too low a maxFileBytes field", payload: `{ "label": null, - "expirationTime":"2025-01-01T00:00:00Z", + "urlExpirationTime":"2025-01-01T00:00:00Z", "maxFileBytes": 1, "maxFileUploads": null }`, @@ -179,7 +217,7 @@ func TestGuestLinksPostRejectsInvalidRequest(t *testing.T) { description: "zero maxFileBytes field", payload: `{ "label": null, - "expirationTime":"2025-01-01T00:00:00Z", + "urlExpirationTime":"2025-01-01T00:00:00Z", "maxFileBytes": 0, "maxFileUploads": null }`, @@ -188,7 +226,7 @@ func TestGuestLinksPostRejectsInvalidRequest(t *testing.T) { description: "negative maxFileUploads field", payload: `{ "label": null, - "expirationTime":"2025-01-01T00:00:00Z", + "urlExpirationTime":"2025-01-01T00:00:00Z", "maxFileBytes": null, "maxFileUploads": -5 }`, @@ -197,7 +235,7 @@ func TestGuestLinksPostRejectsInvalidRequest(t *testing.T) { description: "decimal maxFileUploads field", payload: `{ "label": null, - "expirationTime":"2025-01-01T00:00:00Z", + "urlExpirationTime":"2025-01-01T00:00:00Z", "maxFileBytes": null, "maxFileUploads": 1.5 }`, @@ -206,7 +244,7 @@ func TestGuestLinksPostRejectsInvalidRequest(t *testing.T) { description: "zero maxFileUploads field", payload: `{ "label": null, - "expirationTime":"2025-01-01T00:00:00Z", + "urlExpirationTime":"2025-01-01T00:00:00Z", "maxFileBytes": null, "maxFileUploads": 0 }`, @@ -245,9 +283,9 @@ func makeGuestUploadCountLimit(i int) picoshare.GuestUploadCountLimit { func TestDeleteExistingGuestLink(t *testing.T) { dataStore := test_sqlite.New() dataStore.InsertGuestLink(picoshare.GuestLink{ - ID: picoshare.GuestLinkID("abcdefgh23456789"), - Created: time.Now(), - Expires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + ID: picoshare.GuestLinkID("abcdefgh23456789"), + Created: time.Now(), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), }) s := handlers.New(mockAuthenticator{}, &dataStore, nilSpaceChecker, nilGarbageCollector) diff --git a/handlers/parse/file_lifetime.go b/handlers/parse/file_lifetime.go index 03128a33..58362afc 100644 --- a/handlers/parse/file_lifetime.go +++ b/handlers/parse/file_lifetime.go @@ -2,6 +2,7 @@ package parse import ( "fmt" + "time" "github.com/mtlynch/picoshare/v2/picoshare" ) @@ -14,8 +15,9 @@ const maxFileLifetimeInYears = 10 const daysPerYear = 365 var ( - ErrFileLifetimeTooShort = fmt.Errorf("file lifetime must be at least %d days", minFileLifetimeInDays) - ErrFileLifetimeTooLong = fmt.Errorf("file lifetime must be at most %d years", maxFileLifetimeInYears) + ErrFileLifetimeTooShort = fmt.Errorf("file lifetime must be at least %d days", minFileLifetimeInDays) + ErrFileLifetimeTooLong = fmt.Errorf("file lifetime must be at most %d years", maxFileLifetimeInYears) + ErrFileLifetimeUnrecognizedFormat = fmt.Errorf("unrecognized format for file life time, must be in 1ns, 1us (or 1µs), 1ms, 1s, 1m, 1h format") ) func FileLifetime(lifetimeInDays uint16) (picoshare.FileLifetime, error) { @@ -27,3 +29,16 @@ func FileLifetime(lifetimeInDays uint16) (picoshare.FileLifetime, error) { } return picoshare.NewFileLifetimeInDays(lifetimeInDays), nil } + +func FileLifetimeFromString(fileLifetimeRaw string) (picoshare.FileLifetime, error) { + dur, err := time.ParseDuration(fileLifetimeRaw) + if err != nil { + return picoshare.FileLifetime{}, ErrFileLifetimeUnrecognizedFormat + } + + if flt, _ := picoshare.NewFileLifetimeFromDuration(dur); flt == picoshare.FileLifetimeInfinite { + return picoshare.FileLifetimeInfinite, nil + } + + return picoshare.NewFileLifetimeFromDuration(dur) +} diff --git a/handlers/static/js/controllers/guestLinks.js b/handlers/static/js/controllers/guestLinks.js index e34af32c..233cd6be 100644 --- a/handlers/static/js/controllers/guestLinks.js +++ b/handlers/static/js/controllers/guestLinks.js @@ -2,7 +2,8 @@ export async function guestLinkNew( label, - expirationTime, + urlExpirationTime, + fileLifetime, maxFileBytes, maxFileUploads ) { @@ -11,7 +12,8 @@ export async function guestLinkNew( credentials: "include", body: JSON.stringify({ label, - expirationTime, + urlExpirationTime, + fileLifetime, maxFileBytes, maxFileUploads, }), diff --git a/handlers/templates/pages/guest-link-create.html b/handlers/templates/pages/guest-link-create.html index 8a2983a3..377e402a 100644 --- a/handlers/templates/pages/guest-link-create.html +++ b/handlers/templates/pages/guest-link-create.html @@ -6,6 +6,9 @@ const labelInput = document.getElementById("label"); const expirationSelect = document.getElementById("expiration-select"); + const fileExpirationSelect = document.getElementById( + "file-expiration-select" + ); const maxFileBytesInput = document.getElementById("max-file-size"); const fileUploadLimitInput = document.getElementById("file-upload-limit"); const createLinkForm = document.getElementById("create-guest-link-form"); @@ -22,7 +25,8 @@ function guestLinkFromInputs() { return { label: labelInput.value || null, - expirationTime: expirationSelect.value, + urlExpirationTime: expirationSelect.value, + fileLifetime: fileExpirationSelect.value, maxFileBytes: maxFileBytesInput.valueAsNumber ? megabytesToBytes(maxFileBytesInput.valueAsNumber) : null, @@ -42,7 +46,8 @@ const guestLink = guestLinkFromInputs(); guestLinkNew( guestLink.label, - guestLink.expirationTime, + guestLink.urlExpirationTime, + guestLink.fileLifetime, guestLink.maxFileBytes, guestLink.maxFileUploads ) @@ -102,6 +107,24 @@

Create Guest Link

+
+ +
+
+ +
+
+
+
diff --git a/handlers/templates/pages/guest-link-index.html b/handlers/templates/pages/guest-link-index.html index a69ce155..d34f667f 100644 --- a/handlers/templates/pages/guest-link-index.html +++ b/handlers/templates/pages/guest-link-index.html @@ -120,7 +120,8 @@

Guest Links

Label Created - Expiration + URL Expiration + File Expiration Max Upload Size Uploads Actions @@ -140,7 +141,10 @@

Guest Links

{{ formatDate .Created }} - {{ formatExpiration .Expires }} + {{ formatExpiration .UrlExpires }} + + + {{ .FileLifetime.FriendlyName }} {{ formatSizeLimit .MaxFileBytes }} diff --git a/handlers/upload.go b/handlers/upload.go index 0e83eb84..b7a81389 100644 --- a/handlers/upload.go +++ b/handlers/upload.go @@ -132,7 +132,7 @@ func (s Server) guestEntryPost() http.HandlerFunc { r.Body = http.MaxBytesReader(w, r.Body, int64(*gl.MaxFileBytes)) } - id, err := s.insertFileFromRequest(r, picoshare.NeverExpire, guestLinkID) + id, err := s.insertFileFromRequest(r, gl.FileLifetime.ExpirationFromTime(time.Now()), guestLinkID) if err != nil { var de *dbError if errors.As(err, &de) { diff --git a/handlers/upload_test.go b/handlers/upload_test.go index 0c79b714..ea310045 100644 --- a/handlers/upload_test.go +++ b/handlers/upload_test.go @@ -5,6 +5,7 @@ import ( "bytes" "encoding/json" "io" + "math" "mime/multipart" "net/http" "net/http/httptest" @@ -280,9 +281,10 @@ func TestGuestUpload(t *testing.T) { { description: "valid upload to guest link", guestLinkInStore: picoshare.GuestLink{ - ID: picoshare.GuestLinkID("abcdefgh23456789"), - Created: mustParseTime("2022-01-01T00:00:00Z"), - Expires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + ID: picoshare.GuestLinkID("abcdefgh23456789"), + Created: mustParseTime("2022-01-01T00:00:00Z"), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + FileLifetime: picoshare.FileLifetimeInfinite, }, guestLinkID: "abcdefgh23456789", status: http.StatusOK, @@ -290,9 +292,10 @@ func TestGuestUpload(t *testing.T) { { description: "expired guest link", guestLinkInStore: picoshare.GuestLink{ - ID: picoshare.GuestLinkID("abcdefgh23456789"), - Created: mustParseTime("2000-01-01T00:00:00Z"), - Expires: mustParseExpirationTime("2000-01-02T03:04:25Z"), + ID: picoshare.GuestLinkID("abcdefgh23456789"), + Created: mustParseTime("2000-01-01T00:00:00Z"), + UrlExpires: mustParseExpirationTime("2000-01-02T03:04:25Z"), + FileLifetime: picoshare.FileLifetimeInfinite, }, guestLinkID: "abcdefgh23456789", status: http.StatusUnauthorized, @@ -300,9 +303,9 @@ func TestGuestUpload(t *testing.T) { { description: "invalid guest link", guestLinkInStore: picoshare.GuestLink{ - ID: picoshare.GuestLinkID("abcdefgh23456789"), - Created: mustParseTime("2000-01-01T00:00:00Z"), - Expires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + ID: picoshare.GuestLinkID("abcdefgh23456789"), + Created: mustParseTime("2000-01-01T00:00:00Z"), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), }, guestLinkID: "i-am-an-invalid-guest-link", // Too long status: http.StatusBadRequest, @@ -310,9 +313,9 @@ func TestGuestUpload(t *testing.T) { { description: "invalid guest link", guestLinkInStore: picoshare.GuestLink{ - ID: picoshare.GuestLinkID("abcdefgh23456789"), - Created: mustParseTime("2000-01-01T00:00:00Z"), - Expires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + ID: picoshare.GuestLinkID("abcdefgh23456789"), + Created: mustParseTime("2000-01-01T00:00:00Z"), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), }, guestLinkID: "I0OI0OI0OI0OI0OI", // Contains all invalid characters status: http.StatusBadRequest, @@ -320,9 +323,9 @@ func TestGuestUpload(t *testing.T) { { description: "non-existent guest link", guestLinkInStore: picoshare.GuestLink{ - ID: picoshare.GuestLinkID("abcdefgh23456789"), - Created: mustParseTime("2000-01-01T00:00:00Z"), - Expires: mustParseExpirationTime("2000-01-02T03:04:25Z"), + ID: picoshare.GuestLinkID("abcdefgh23456789"), + Created: mustParseTime("2000-01-01T00:00:00Z"), + UrlExpires: mustParseExpirationTime("2000-01-02T03:04:25Z"), }, guestLinkID: "doesntexistaaaaa", status: http.StatusNotFound, @@ -330,9 +333,10 @@ func TestGuestUpload(t *testing.T) { { description: "reject upload that includes a note", guestLinkInStore: picoshare.GuestLink{ - ID: picoshare.GuestLinkID("abcdefgh23456789"), - Created: mustParseTime("2022-01-01T00:00:00Z"), - Expires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + ID: picoshare.GuestLinkID("abcdefgh23456789"), + Created: mustParseTime("2022-01-01T00:00:00Z"), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + FileLifetime: picoshare.FileLifetimeInfinite, }, guestLinkID: "abcdefgh23456789", note: "I'm a disallowed note", @@ -343,8 +347,9 @@ func TestGuestUpload(t *testing.T) { guestLinkInStore: picoshare.GuestLink{ ID: picoshare.GuestLinkID("abcdefgh23456789"), Created: mustParseTime("2000-01-01T00:00:00Z"), - Expires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), MaxFileUploads: makeGuestUploadCountLimit(2), + FileLifetime: picoshare.FileLifetimeInfinite, }, entriesInStore: []picoshare.UploadEntry{ { @@ -372,12 +377,35 @@ func TestGuestUpload(t *testing.T) { guestLinkInStore: picoshare.GuestLink{ ID: picoshare.GuestLinkID("abcdefgh23456789"), Created: mustParseTime("2000-01-01T00:00:00Z"), - Expires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), MaxFileBytes: makeGuestUploadMaxFileBytes(1), + FileLifetime: picoshare.FileLifetimeInfinite, }, guestLinkID: "abcdefgh23456789", status: http.StatusBadRequest, }, + { + description: "guest file expires in 1 day", + guestLinkInStore: picoshare.GuestLink{ + ID: picoshare.GuestLinkID("abcdefgh23456789"), + Created: mustParseTime("2022-01-01T00:00:00Z"), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + FileLifetime: picoshare.NewFileLifetimeInDays(1), + }, + guestLinkID: "abcdefgh23456789", + status: http.StatusOK, + }, + { + description: "guest file expires in 365 day", + guestLinkInStore: picoshare.GuestLink{ + ID: picoshare.GuestLinkID("abcdefgh23456789"), + Created: mustParseTime("2022-01-01T00:00:00Z"), + UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"), + FileLifetime: picoshare.NewFileLifetimeInDays(365), + }, + guestLinkID: "abcdefgh23456789", + status: http.StatusOK, + }, } { t.Run(tt.description, func(t *testing.T) { store := test_sqlite.New() @@ -440,14 +468,21 @@ func TestGuestUpload(t *testing.T) { t.Errorf("filename=%v, want=%v", got, want) } - // Guest uploads never expire. - if got, want := entry.Expires, picoshare.NeverExpire; got != want { + if got, want := convertExpirationTimeToFileLifetime(entry.Expires), tt.guestLinkInStore.FileLifetime; got != want { t.Errorf("expiration=%v, want=%v", got, want) } }) } } +func convertExpirationTimeToFileLifetime(et picoshare.ExpirationTime) picoshare.FileLifetime { + if et == picoshare.NeverExpire { + return picoshare.FileLifetimeInfinite + } + delta := math.Round(time.Until(time.Time(et)).Hours() / 24) + return picoshare.NewFileLifetimeInDays(uint16(delta)) +} + func createMultipartFormBody(filename, note string, r io.Reader) (io.Reader, string) { var b bytes.Buffer bw := bufio.NewWriter(&b) diff --git a/handlers/views.go b/handlers/views.go index b4f3119d..42e26dd0 100644 --- a/handlers/views.go +++ b/handlers/views.go @@ -123,6 +123,9 @@ func (s Server) guestLinksNewGet() http.HandlerFunc { "formatExpiration": func(t time.Time) string { return t.Format(time.RFC3339) }, + "formatLifetime": func(flt picoshare.FileLifetime) string { + return flt.Duration().String() + }, } t := parseTemplatesWithFuncs(fns, "templates/pages/guest-link-create.html") @@ -133,9 +136,14 @@ func (s Server) guestLinksNewGet() http.HandlerFunc { Expiration time.Time IsDefault bool } + type fileLifetimeOption struct { + FileLifetime picoshare.FileLifetime + IsDefault bool + } if err := t.Execute(w, struct { commonProps - ExpirationOptions []expirationOption + ExpirationOptions []expirationOption + FileLifetimeOptions []fileLifetimeOption }{ commonProps: makeCommonProps("PicoShare - New Guest Link", r.Context()), ExpirationOptions: []expirationOption{ @@ -145,6 +153,13 @@ func (s Server) guestLinksNewGet() http.HandlerFunc { {"1 year", time.Now().AddDate(1, 0, 0), false}, {"Never", time.Time(picoshare.NeverExpire), true}, }, + FileLifetimeOptions: []fileLifetimeOption{ + {picoshare.NewFileLifetimeInDays(1), false}, + {picoshare.NewFileLifetimeInDays(7), false}, + {picoshare.NewFileLifetimeInDays(30), false}, + {picoshare.NewFileLifetimeInYears(1), false}, + {picoshare.FileLifetimeInfinite, true}, + }, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -483,7 +498,6 @@ func (s Server) uploadGet() http.HandlerFunc { friendlyName := lto.Lifetime.FriendlyName() expiration := time.Now().Add(lto.Lifetime.Duration()) if lto.Lifetime.Equal(picoshare.FileLifetimeInfinite) { - friendlyName = "Never" expiration = time.Time(picoshare.NeverExpire) } expirationOptions = append(expirationOptions, expirationOption{ diff --git a/picoshare/file_lifetime.go b/picoshare/file_lifetime.go index 8cdfdb0e..38cf4461 100644 --- a/picoshare/file_lifetime.go +++ b/picoshare/file_lifetime.go @@ -19,6 +19,16 @@ type FileLifetime struct { // not expire, effectively. var FileLifetimeInfinite = NewFileLifetimeInYears(100) +func NewFileLifetimeFromDuration(duration time.Duration) (FileLifetime, error) { + + if duration < (time.Hour * 24) { + return FileLifetime{}, fmt.Errorf("file lifetime must be at least 1 day") + } + return FileLifetime{ + d: duration, + }, nil +} + func NewFileLifetimeInDays(days uint16) FileLifetime { return FileLifetime{ d: hoursPerDay * time.Hour * time.Duration(days), @@ -47,6 +57,9 @@ func (lt FileLifetime) IsYearBoundary() bool { } func (lt FileLifetime) FriendlyName() string { + if lt == FileLifetimeInfinite { + return "Never" + } value := lt.Days() unit := "day" if lt.IsYearBoundary() { @@ -62,3 +75,7 @@ func (lt FileLifetime) FriendlyName() string { func (lt FileLifetime) Equal(o FileLifetime) bool { return lt.d == o.d } + +func (lt FileLifetime) ExpirationFromTime(t time.Time) ExpirationTime { + return ExpirationTime(t.Add(lt.d)) +} diff --git a/picoshare/file_lifetime_test.go b/picoshare/file_lifetime_test.go index 8027a6fa..561c3e76 100644 --- a/picoshare/file_lifetime_test.go +++ b/picoshare/file_lifetime_test.go @@ -56,6 +56,11 @@ func TestFileLifetime(t *testing.T) { isOnYearBoundary: false, friendlyName: "10 years", }, + { + lifetime: picoshare.FileLifetimeInfinite, + isOnYearBoundary: false, + friendlyName: "Never", + }, } { t.Run(tt.friendlyName, func(t *testing.T) { if got, want := tt.lifetime.FriendlyName(), tt.friendlyName; got != want { diff --git a/picoshare/guest_link.go b/picoshare/guest_link.go index c9ffa9b3..591cb9e5 100644 --- a/picoshare/guest_link.go +++ b/picoshare/guest_link.go @@ -14,7 +14,8 @@ type ( ID GuestLinkID Label GuestLinkLabel Created time.Time - Expires ExpirationTime + UrlExpires ExpirationTime + FileLifetime FileLifetime MaxFileBytes GuestUploadMaxFileBytes MaxFileUploads GuestUploadCountLimit FilesUploaded int @@ -46,10 +47,10 @@ func (gl GuestLink) CanAcceptMoreFiles() bool { } func (gl GuestLink) IsExpired() bool { - if gl.Expires == NeverExpire { + if gl.UrlExpires == NeverExpire { return false } - return time.Now().After(time.Time(gl.Expires)) + return time.Now().After(time.Time(gl.UrlExpires)) } func (gl GuestLink) IsActive() bool { diff --git a/store/sqlite/guest_links.go b/store/sqlite/guest_links.go index 88edc116..7f784e53 100644 --- a/store/sqlite/guest_links.go +++ b/store/sqlite/guest_links.go @@ -18,7 +18,8 @@ func (s Store) GetGuestLink(id picoshare.GuestLinkID) (picoshare.GuestLink, erro guest_links.max_file_bytes AS max_file_bytes, guest_links.max_file_uploads AS max_file_uploads, guest_links.creation_time AS creation_time, - guest_links.expiration_time AS expiration_time, + guest_links.url_expiration_time AS url_expiration_time, + guest_links.file_expiration_time AS file_expiration_time, SUM(CASE WHEN entries.id IS NOT NULL THEN 1 ELSE 0 END) AS entry_count FROM guest_links @@ -40,7 +41,8 @@ func (s Store) GetGuestLinks() ([]picoshare.GuestLink, error) { guest_links.max_file_bytes AS max_file_bytes, guest_links.max_file_uploads AS max_file_uploads, guest_links.creation_time AS creation_time, - guest_links.expiration_time AS expiration_time, + guest_links.url_expiration_time AS url_expiration_time, + guest_links.file_expiration_time AS file_expiration_time, SUM(CASE WHEN entries.id IS NOT NULL THEN 1 ELSE 0 END) AS entry_count FROM guest_links @@ -76,16 +78,18 @@ func (s *Store) InsertGuestLink(guestLink picoshare.GuestLink) error { max_file_bytes, max_file_uploads, creation_time, - expiration_time + url_expiration_time, + file_expiration_time ) - VALUES (:id, :label, :max_file_bytes, :max_file_uploads, :creation_time, :expiration_time) + VALUES (:id, :label, :max_file_bytes, :max_file_uploads, :creation_time, :url_expiration_time, :file_expiration_time) `, sql.Named("id", guestLink.ID), sql.Named("label", guestLink.Label), sql.Named("max_file_bytes", guestLink.MaxFileBytes), sql.Named("max_file_uploads", guestLink.MaxFileUploads), sql.Named("creation_time", formatTime(time.Now())), - sql.Named("expiration_time", formatExpirationTime(guestLink.Expires))); err != nil { + sql.Named("url_expiration_time", formatExpirationTime(guestLink.UrlExpires)), + sql.Named("file_expiration_time", formatFileLifetime(guestLink.FileLifetime))); err != nil { return err } @@ -129,10 +133,11 @@ func guestLinkFromRow(row rowScanner) (picoshare.GuestLink, error) { var maxFileBytes picoshare.GuestUploadMaxFileBytes var maxFileUploads picoshare.GuestUploadCountLimit var creationTimeRaw string - var expirationTimeRaw string + var urlExpirationTimeRaw string + var fileLifetimeRaw *string var filesUploaded int - err := row.Scan(&id, &label, &maxFileBytes, &maxFileUploads, &creationTimeRaw, &expirationTimeRaw, &filesUploaded) + err := row.Scan(&id, &label, &maxFileBytes, &maxFileUploads, &creationTimeRaw, &urlExpirationTimeRaw, &fileLifetimeRaw, &filesUploaded) if err == sql.ErrNoRows { return picoshare.GuestLink{}, store.GuestLinkNotFoundError{ID: id} } else if err != nil { @@ -144,11 +149,21 @@ func guestLinkFromRow(row rowScanner) (picoshare.GuestLink, error) { return picoshare.GuestLink{}, err } - et, err := parseDatetime(expirationTimeRaw) + uet, err := parseDatetime(urlExpirationTimeRaw) if err != nil { return picoshare.GuestLink{}, err } + var fileLifetime picoshare.FileLifetime + if fileLifetimeRaw == nil { + fileLifetime = picoshare.FileLifetimeInfinite + } else { + fileLifetime, err = parseFileLifetime(*fileLifetimeRaw) + if err != nil { + return picoshare.GuestLink{}, err + } + } + return picoshare.GuestLink{ ID: id, Label: label, @@ -156,6 +171,7 @@ func guestLinkFromRow(row rowScanner) (picoshare.GuestLink, error) { MaxFileUploads: maxFileUploads, FilesUploaded: filesUploaded, Created: ct, - Expires: picoshare.ExpirationTime(et), + UrlExpires: picoshare.ExpirationTime(uet), + FileLifetime: fileLifetime, }, nil } diff --git a/store/sqlite/migrations/008-migrate-to-guest-file-expiration.sql b/store/sqlite/migrations/008-migrate-to-guest-file-expiration.sql new file mode 100644 index 00000000..1a0f419b --- /dev/null +++ b/store/sqlite/migrations/008-migrate-to-guest-file-expiration.sql @@ -0,0 +1,5 @@ +ALTER TABLE guest_links +RENAME COLUMN expiration_time TO url_expiration_time; + +ALTER TABLE guest_links +ADD file_expiration_time TEXT; diff --git a/store/sqlite/sqlite.go b/store/sqlite/sqlite.go index 0892179c..f1740d16 100644 --- a/store/sqlite/sqlite.go +++ b/store/sqlite/sqlite.go @@ -74,6 +74,18 @@ func formatTime(t time.Time) string { return t.UTC().Format(timeFormat) } +func formatFileLifetime(lt picoshare.FileLifetime) string { + return lt.Duration().String() +} + func parseDatetime(s string) (time.Time, error) { return time.Parse(timeFormat, s) } + +func parseFileLifetime(s string) (picoshare.FileLifetime, error) { + d, err := time.ParseDuration(s) + if err != nil { + return picoshare.FileLifetime{}, err + } + return picoshare.NewFileLifetimeFromDuration(d) +}