feat(boxstore): add configurable expiry for one-time downloads
Introduces a new configuration setting `one_time_download_expiry_seconds` to allow administrators to define a default expiration period for one-time downloads. The retention logic in `boxstore` has been updated to use this global expiry value when a box is marked as a one-time download and no specific retention period is defined in the manifest.
This commit is contained in:
@@ -31,8 +31,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
uploadRoot = filepath.Join("data", "uploads")
|
uploadRoot = filepath.Join("data", "uploads")
|
||||||
manifestMu sync.Mutex
|
oneTimeDownloadExpiry int64
|
||||||
|
manifestMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
var retentionOptions = []models.RetentionOption{
|
var retentionOptions = []models.RetentionOption{
|
||||||
@@ -70,6 +71,10 @@ func SetUploadRoot(path string) {
|
|||||||
uploadRoot = filepath.Clean(path)
|
uploadRoot = filepath.Clean(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetOneTimeDownloadExpiry(seconds int64) {
|
||||||
|
oneTimeDownloadExpiry = seconds
|
||||||
|
}
|
||||||
|
|
||||||
func UploadRoot() string {
|
func UploadRoot() string {
|
||||||
return uploadRoot
|
return uploadRoot
|
||||||
}
|
}
|
||||||
@@ -638,7 +643,17 @@ func startRetentionIfTerminalUnlocked(manifest *models.BoxManifest) {
|
|||||||
if !manifest.ExpiresAt.IsZero() || len(manifest.Files) == 0 {
|
if !manifest.ExpiresAt.IsZero() || len(manifest.Files) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if manifest.OneTimeDownload {
|
|
||||||
|
seconds := manifest.RetentionSecs
|
||||||
|
if seconds <= 0 {
|
||||||
|
if manifest.OneTimeDownload {
|
||||||
|
seconds = oneTimeDownloadExpiry
|
||||||
|
} else {
|
||||||
|
seconds = normalizeRetentionOption(manifest.RetentionKey).Seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if manifest.OneTimeDownload && seconds <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,10 +663,7 @@ func startRetentionIfTerminalUnlocked(manifest *models.BoxManifest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
seconds := manifest.RetentionSecs
|
// seconds is already handled above
|
||||||
if seconds <= 0 {
|
|
||||||
seconds = normalizeRetentionOption(manifest.RetentionKey).Seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retention starts after uploads settle so slow or very large uploads do
|
// Retention starts after uploads settle so slow or very large uploads do
|
||||||
// not expire before users get a real chance to open the box.
|
// not expire before users get a real chance to open the box.
|
||||||
|
|||||||
@@ -26,23 +26,24 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SettingGuestUploadsEnabled = "guest_uploads_enabled"
|
SettingGuestUploadsEnabled = "guest_uploads_enabled"
|
||||||
SettingAPIEnabled = "api_enabled"
|
SettingAPIEnabled = "api_enabled"
|
||||||
SettingZipDownloadsEnabled = "zip_downloads_enabled"
|
SettingZipDownloadsEnabled = "zip_downloads_enabled"
|
||||||
SettingOneTimeDownloadsEnabled = "one_time_downloads_enabled"
|
SettingOneTimeDownloadsEnabled = "one_time_downloads_enabled"
|
||||||
SettingRenewOnAccessEnabled = "renew_on_access_enabled"
|
SettingOneTimeDownloadExpirySecs = "one_time_download_expiry_seconds"
|
||||||
SettingRenewOnDownloadEnabled = "renew_on_download_enabled"
|
SettingRenewOnAccessEnabled = "renew_on_access_enabled"
|
||||||
SettingDefaultGuestExpirySecs = "default_guest_expiry_seconds"
|
SettingRenewOnDownloadEnabled = "renew_on_download_enabled"
|
||||||
SettingMaxGuestExpirySecs = "max_guest_expiry_seconds"
|
SettingDefaultGuestExpirySecs = "default_guest_expiry_seconds"
|
||||||
SettingGlobalMaxFileSizeBytes = "global_max_file_size_bytes"
|
SettingMaxGuestExpirySecs = "max_guest_expiry_seconds"
|
||||||
SettingGlobalMaxBoxSizeBytes = "global_max_box_size_bytes"
|
SettingGlobalMaxFileSizeBytes = "global_max_file_size_bytes"
|
||||||
SettingDefaultUserMaxFileBytes = "default_user_max_file_size_bytes"
|
SettingGlobalMaxBoxSizeBytes = "global_max_box_size_bytes"
|
||||||
SettingDefaultUserMaxBoxBytes = "default_user_max_box_size_bytes"
|
SettingDefaultUserMaxFileBytes = "default_user_max_file_size_bytes"
|
||||||
SettingSessionTTLSeconds = "session_ttl_seconds"
|
SettingDefaultUserMaxBoxBytes = "default_user_max_box_size_bytes"
|
||||||
SettingBoxPollIntervalMS = "box_poll_interval_ms"
|
SettingSessionTTLSeconds = "session_ttl_seconds"
|
||||||
SettingThumbnailBatchSize = "thumbnail_batch_size"
|
SettingBoxPollIntervalMS = "box_poll_interval_ms"
|
||||||
SettingThumbnailIntervalSeconds = "thumbnail_interval_seconds"
|
SettingThumbnailBatchSize = "thumbnail_batch_size"
|
||||||
SettingDataDir = "data_dir"
|
SettingThumbnailIntervalSeconds = "thumbnail_interval_seconds"
|
||||||
|
SettingDataDir = "data_dir"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SettingType string
|
type SettingType string
|
||||||
@@ -82,12 +83,13 @@ type Config struct {
|
|||||||
AdminCookieSecure bool
|
AdminCookieSecure bool
|
||||||
AllowAdminSettingsOverride bool
|
AllowAdminSettingsOverride bool
|
||||||
|
|
||||||
GuestUploadsEnabled bool
|
GuestUploadsEnabled bool
|
||||||
APIEnabled bool
|
APIEnabled bool
|
||||||
ZipDownloadsEnabled bool
|
ZipDownloadsEnabled bool
|
||||||
OneTimeDownloadsEnabled bool
|
OneTimeDownloadsEnabled bool
|
||||||
RenewOnAccessEnabled bool
|
OneTimeDownloadExpirySeconds int64
|
||||||
RenewOnDownloadEnabled bool
|
RenewOnAccessEnabled bool
|
||||||
|
RenewOnDownloadEnabled bool
|
||||||
|
|
||||||
DefaultGuestExpirySeconds int64
|
DefaultGuestExpirySeconds int64
|
||||||
MaxGuestExpirySeconds int64
|
MaxGuestExpirySeconds int64
|
||||||
@@ -110,6 +112,7 @@ var Definitions = []SettingDefinition{
|
|||||||
{Key: SettingAPIEnabled, EnvName: "WARPBOX_API_ENABLED", Label: "API enabled", Type: SettingTypeBool, Editable: true},
|
{Key: SettingAPIEnabled, EnvName: "WARPBOX_API_ENABLED", Label: "API enabled", Type: SettingTypeBool, Editable: true},
|
||||||
{Key: SettingZipDownloadsEnabled, EnvName: "WARPBOX_ZIP_DOWNLOADS_ENABLED", Label: "ZIP downloads enabled", Type: SettingTypeBool, Editable: true},
|
{Key: SettingZipDownloadsEnabled, EnvName: "WARPBOX_ZIP_DOWNLOADS_ENABLED", Label: "ZIP downloads enabled", Type: SettingTypeBool, Editable: true},
|
||||||
{Key: SettingOneTimeDownloadsEnabled, EnvName: "WARPBOX_ONE_TIME_DOWNLOADS_ENABLED", Label: "One-time downloads enabled", Type: SettingTypeBool, Editable: true},
|
{Key: SettingOneTimeDownloadsEnabled, EnvName: "WARPBOX_ONE_TIME_DOWNLOADS_ENABLED", Label: "One-time downloads enabled", Type: SettingTypeBool, Editable: true},
|
||||||
|
{Key: SettingOneTimeDownloadExpirySecs, EnvName: "WARPBOX_ONE_TIME_DOWNLOAD_EXPIRY_SECONDS", Label: "One-time download expiry seconds", Type: SettingTypeInt64, Editable: true, Minimum: 0},
|
||||||
{Key: SettingRenewOnAccessEnabled, EnvName: "WARPBOX_RENEW_ON_ACCESS_ENABLED", Label: "Renew on access enabled", Type: SettingTypeBool, Editable: true},
|
{Key: SettingRenewOnAccessEnabled, EnvName: "WARPBOX_RENEW_ON_ACCESS_ENABLED", Label: "Renew on access enabled", Type: SettingTypeBool, Editable: true},
|
||||||
{Key: SettingRenewOnDownloadEnabled, EnvName: "WARPBOX_RENEW_ON_DOWNLOAD_ENABLED", Label: "Renew on download enabled", Type: SettingTypeBool, Editable: true},
|
{Key: SettingRenewOnDownloadEnabled, EnvName: "WARPBOX_RENEW_ON_DOWNLOAD_ENABLED", Label: "Renew on download enabled", Type: SettingTypeBool, Editable: true},
|
||||||
{Key: SettingDefaultGuestExpirySecs, EnvName: "WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS", Label: "Default guest expiry seconds", Type: SettingTypeInt64, Editable: true, Minimum: 0},
|
{Key: SettingDefaultGuestExpirySecs, EnvName: "WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS", Label: "Default guest expiry seconds", Type: SettingTypeInt64, Editable: true, Minimum: 0},
|
||||||
@@ -126,22 +129,23 @@ var Definitions = []SettingDefinition{
|
|||||||
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
DataDir: "./data",
|
DataDir: "./data",
|
||||||
AdminUsername: "admin",
|
AdminUsername: "admin",
|
||||||
AdminEnabled: AdminEnabledAuto,
|
AdminEnabled: AdminEnabledAuto,
|
||||||
AllowAdminSettingsOverride: true,
|
AllowAdminSettingsOverride: true,
|
||||||
GuestUploadsEnabled: true,
|
GuestUploadsEnabled: true,
|
||||||
APIEnabled: true,
|
APIEnabled: true,
|
||||||
ZipDownloadsEnabled: true,
|
ZipDownloadsEnabled: true,
|
||||||
OneTimeDownloadsEnabled: true,
|
OneTimeDownloadsEnabled: true,
|
||||||
DefaultGuestExpirySeconds: 10,
|
OneTimeDownloadExpirySeconds: 7 * 24 * 60 * 60,
|
||||||
MaxGuestExpirySeconds: 48 * 60 * 60,
|
DefaultGuestExpirySeconds: 10,
|
||||||
SessionTTLSeconds: 24 * 60 * 60,
|
MaxGuestExpirySeconds: 48 * 60 * 60,
|
||||||
BoxPollIntervalMS: 5000,
|
SessionTTLSeconds: 24 * 60 * 60,
|
||||||
ThumbnailBatchSize: 10,
|
BoxPollIntervalMS: 5000,
|
||||||
ThumbnailIntervalSeconds: 30,
|
ThumbnailBatchSize: 10,
|
||||||
sources: make(map[string]Source),
|
ThumbnailIntervalSeconds: 30,
|
||||||
values: make(map[string]string),
|
sources: make(map[string]Source),
|
||||||
|
values: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.captureDefaults()
|
cfg.captureDefaults()
|
||||||
@@ -198,6 +202,7 @@ func Load() (*Config, error) {
|
|||||||
}{
|
}{
|
||||||
{SettingDefaultGuestExpirySecs, "WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS", 0, &cfg.DefaultGuestExpirySeconds},
|
{SettingDefaultGuestExpirySecs, "WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS", 0, &cfg.DefaultGuestExpirySeconds},
|
||||||
{SettingMaxGuestExpirySecs, "WARPBOX_MAX_GUEST_EXPIRY_SECONDS", 0, &cfg.MaxGuestExpirySeconds},
|
{SettingMaxGuestExpirySecs, "WARPBOX_MAX_GUEST_EXPIRY_SECONDS", 0, &cfg.MaxGuestExpirySeconds},
|
||||||
|
{SettingOneTimeDownloadExpirySecs, "WARPBOX_ONE_TIME_DOWNLOAD_EXPIRY_SECONDS", 0, &cfg.OneTimeDownloadExpirySeconds},
|
||||||
{SettingSessionTTLSeconds, "WARPBOX_SESSION_TTL_SECONDS", 60, &cfg.SessionTTLSeconds},
|
{SettingSessionTTLSeconds, "WARPBOX_SESSION_TTL_SECONDS", 60, &cfg.SessionTTLSeconds},
|
||||||
}
|
}
|
||||||
for _, item := range envInt64s {
|
for _, item := range envInt64s {
|
||||||
@@ -359,6 +364,7 @@ func (cfg *Config) captureDefaults() {
|
|||||||
cfg.setValue(SettingAPIEnabled, formatBool(cfg.APIEnabled), SourceDefault)
|
cfg.setValue(SettingAPIEnabled, formatBool(cfg.APIEnabled), SourceDefault)
|
||||||
cfg.setValue(SettingZipDownloadsEnabled, formatBool(cfg.ZipDownloadsEnabled), SourceDefault)
|
cfg.setValue(SettingZipDownloadsEnabled, formatBool(cfg.ZipDownloadsEnabled), SourceDefault)
|
||||||
cfg.setValue(SettingOneTimeDownloadsEnabled, formatBool(cfg.OneTimeDownloadsEnabled), SourceDefault)
|
cfg.setValue(SettingOneTimeDownloadsEnabled, formatBool(cfg.OneTimeDownloadsEnabled), SourceDefault)
|
||||||
|
cfg.setValue(SettingOneTimeDownloadExpirySecs, strconv.FormatInt(cfg.OneTimeDownloadExpirySeconds, 10), SourceDefault)
|
||||||
cfg.setValue(SettingRenewOnAccessEnabled, formatBool(cfg.RenewOnAccessEnabled), SourceDefault)
|
cfg.setValue(SettingRenewOnAccessEnabled, formatBool(cfg.RenewOnAccessEnabled), SourceDefault)
|
||||||
cfg.setValue(SettingRenewOnDownloadEnabled, formatBool(cfg.RenewOnDownloadEnabled), SourceDefault)
|
cfg.setValue(SettingRenewOnDownloadEnabled, formatBool(cfg.RenewOnDownloadEnabled), SourceDefault)
|
||||||
cfg.setValue(SettingDefaultGuestExpirySecs, strconv.FormatInt(cfg.DefaultGuestExpirySeconds, 10), SourceDefault)
|
cfg.setValue(SettingDefaultGuestExpirySecs, strconv.FormatInt(cfg.DefaultGuestExpirySeconds, 10), SourceDefault)
|
||||||
@@ -485,6 +491,8 @@ func (cfg *Config) assignInt64(key string, value int64, source Source) {
|
|||||||
cfg.DefaultGuestExpirySeconds = value
|
cfg.DefaultGuestExpirySeconds = value
|
||||||
case SettingMaxGuestExpirySecs:
|
case SettingMaxGuestExpirySecs:
|
||||||
cfg.MaxGuestExpirySeconds = value
|
cfg.MaxGuestExpirySeconds = value
|
||||||
|
case SettingOneTimeDownloadExpirySecs:
|
||||||
|
cfg.OneTimeDownloadExpirySeconds = value
|
||||||
case SettingDefaultUserMaxFileBytes:
|
case SettingDefaultUserMaxFileBytes:
|
||||||
cfg.DefaultUserMaxFileSizeBytes = value
|
cfg.DefaultUserMaxFileSizeBytes = value
|
||||||
case SettingDefaultUserMaxBoxBytes:
|
case SettingDefaultUserMaxBoxBytes:
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ type BoxManifest struct {
|
|||||||
AuthToken string `json:"auth_token,omitempty"`
|
AuthToken string `json:"auth_token,omitempty"`
|
||||||
DisableZip bool `json:"disable_zip,omitempty"`
|
DisableZip bool `json:"disable_zip,omitempty"`
|
||||||
OneTimeDownload bool `json:"one_time_download,omitempty"`
|
OneTimeDownload bool `json:"one_time_download,omitempty"`
|
||||||
|
Consumed bool `json:"consumed,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BoxSummary struct {
|
type BoxSummary struct {
|
||||||
|
|||||||
@@ -211,8 +211,14 @@ func (app *App) handleOneTimeDownloadBox(ctx *gin.Context, boxID string) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !hasManifest || !manifest.OneTimeDownload {
|
if !hasManifest || !manifest.OneTimeDownload || manifest.Consumed {
|
||||||
ctx.String(http.StatusNotFound, "Box not found")
|
ctx.String(http.StatusGone, "Box already consumed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest.Consumed = true
|
||||||
|
if err := boxstore.WriteManifest(boxID, manifest); err != nil {
|
||||||
|
ctx.String(http.StatusInternalServerError, "Could not mark box as consumed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +333,12 @@ func (app *App) handleDownloadThumbnail(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, _, authorized := app.authorizeBoxRequest(ctx, boxID, true); !authorized {
|
manifest, hasManifest, authorized := app.authorizeBoxRequest(ctx, boxID, true)
|
||||||
|
if !authorized {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if hasManifest && manifest.OneTimeDownload {
|
||||||
|
ctx.String(http.StatusForbidden, "Thumbnails disabled for one-time boxes")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func Run(addr string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boxstore.SetUploadRoot(cfg.UploadsDir)
|
boxstore.SetUploadRoot(cfg.UploadsDir)
|
||||||
|
boxstore.SetOneTimeDownloadExpiry(cfg.OneTimeDownloadExpirySeconds)
|
||||||
|
|
||||||
store, err := metastore.Open(cfg.DBDir)
|
store, err := metastore.Open(cfg.DBDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -387,12 +387,51 @@ textarea:disabled {
|
|||||||
:root { --base-font-size: 18px; --ui-scale: 1.88; }
|
:root { --base-font-size: 18px; --ui-scale: 1.88; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
.start-upload-cta {
|
||||||
*,
|
min-width: 128px;
|
||||||
*::before,
|
position: relative;
|
||||||
*::after {
|
overflow: visible;
|
||||||
animation-duration: 1ms !important;
|
isolation: isolate;
|
||||||
animation-iteration-count: 1 !important;
|
font-weight: bold;
|
||||||
scroll-behavior: auto !important;
|
}
|
||||||
}
|
|
||||||
|
.start-upload-cta.is-current-step {
|
||||||
|
animation: start-ready-rainbow-breathe 1150ms ease-in-out infinite;
|
||||||
|
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf, 0 0 0 1px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-upload-cta.is-current-step::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -4px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 4px;
|
||||||
|
background: linear-gradient(90deg, #ff004c, #ffcc00, #00d26a, #00a2ff, #8c48ff, #ff004c, #ffcc00);
|
||||||
|
background-size: 280% 100%;
|
||||||
|
opacity: .9;
|
||||||
|
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
animation: start-border-rainbow-slide 1850ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes start-ready-rainbow-breathe {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.02); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes start-border-rainbow-slide {
|
||||||
|
from { background-position: 0% 0%; }
|
||||||
|
to { background-position: 200% 0%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 1ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<button class="box-address" type="button" id="box-address" data-copy-url="{{ .BoxID }}" title="Copy current page URL">{{ .BoxID }}</button>
|
<button class="box-address" type="button" id="box-address" data-copy-url="{{ .BoxID }}" title="Copy current page URL">{{ .BoxID }}</button>
|
||||||
<a class="win98-button box-toolbar-button" href="/"><img src="/static/img/icons/directory_open_cool-4.png" alt="" aria-hidden="true"><span>Upload</span></a>
|
<a class="win98-button box-toolbar-button" href="/"><img src="/static/img/icons/directory_open_cool-4.png" alt="" aria-hidden="true"><span>Upload</span></a>
|
||||||
{{ if .DownloadAll }}
|
{{ if .DownloadAll }}
|
||||||
<a class="win98-button box-toolbar-button" href="{{ .DownloadAll }}"><img src="/static/img/icons/Windows Icons - PNG/zipfldr.dll_14_101-0.png" alt="" aria-hidden="true"><span>Download Zip</span></a>
|
<a class="win98-button box-toolbar-button {{ if or .ZipOnly (gt (len .Files) 1) }}start-upload-cta is-current-step{{ end }}" href="{{ .DownloadAll }}"><img src="/static/img/icons/Windows Icons - PNG/zipfldr.dll_14_101-0.png" alt="" aria-hidden="true"><span>Download Zip</span></a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user