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)
+}
|