Compare commits

...

30 Commits

Author SHA1 Message Date
binwiederhier
835d1faac4 Bump 2026-03-27 14:14:31 -04:00
binwiederhier
67fc7fe96a Disable HTTP2 for S3 backend with ?disable_http2=true option 2026-03-27 13:59:07 -04:00
binwiederhier
92fa88cf12 Merge branch 'main' into attachment-no-http2 2026-03-27 10:04:42 -04:00
binwiederhier
b5fee121d7 Empty commit 2026-03-26 22:58:24 -04:00
binwiederhier
e1a344339f Dummy 2026-03-26 22:20:07 -04:00
binwiederhier
ae1ecfa1e9 BUmp 2026-03-26 22:17:54 -04:00
binwiederhier
874bdcf9f1 No HTTP/2 2026-03-26 20:47:06 -04:00
binwiederhier
2235d44726 Update changelog 2026-03-26 13:39:04 -04:00
binwiederhier
27bbb10a31 Bump 2026-03-26 11:13:43 -04:00
binwiederhier
be4134fc3b Try to fix flaky again 2026-03-26 10:49:39 -04:00
binwiederhier
e19ba059b5 Make attachment.Close() synchronous 2026-03-26 09:57:06 -04:00
binwiederhier
11a14d8fe7 Fix CI 2026-03-26 09:36:34 -04:00
binwiederhier
3759ff26b4 "npm ci" 2026-03-26 09:28:05 -04:00
binwiederhier
136b50f926 I'm tired and GitHub Actions hates me. I'll release this tomorrow ... 2026-03-25 21:35:51 -04:00
binwiederhier
2770f65027 Arrrg 2026-03-25 21:12:27 -04:00
binwiederhier
db6f813386 Release notes derp 2026-03-25 21:03:49 -04:00
binwiederhier
15d963cb53 Fix flaky test due to new attachment expiry cap logic 2026-03-25 21:00:58 -04:00
binwiederhier
a2206dba9f Fix races in tests 2026-03-25 20:47:20 -04:00
binwiederhier
d159580ecf Bump 2026-03-25 20:34:48 -04:00
binwiederhier
0de9dc11ad Bump 2026-03-25 20:29:12 -04:00
binwiederhier
f790143b0b Refined wording, review 2026-03-25 17:35:24 -04:00
binwiederhier
42b0254c9b Merge branch 'main' of github.com:binwiederhier/ntfy into ageru/main 2026-03-25 17:26:49 -04:00
Philipp C. Heckel
d183af61fa Merge pull request #1672 from binwiederhier/attachment-fixes
Attachment fixes to address inconsistencies between DB and backend store
2026-03-25 17:26:18 -04:00
ageru
e22a77d4bb Rename ntfy-server.openrc to ntfy.openrc 2026-03-24 23:38:16 +01:00
ageru
e0362dce36 install.md - Install ntfy server service manually
Rework of steps to install ntfy server service on systemd and OpenRC
2026-03-24 23:36:19 +01:00
ageru
676f1ff1cb Merge branch 'binwiederhier:main' into main 2026-03-24 22:37:38 +01:00
ageru
e8199fa6b5 install.md - Install ntfy server service manually
First draft - Add manual steps to install ntfy server service on systemd and OpenRC
2026-03-16 23:30:21 +01:00
ageru
c29a7bc8cc Merge branch 'binwiederhier:main' into main 2026-03-16 22:35:14 +01:00
ageru
78f0593abe Merge pull request #1 from ageru/add-openrc-init-file-for-ntfy-server
Create Init service file for OpenRC
2026-03-11 22:00:03 +01:00
ageru
aca58f040f Create Init service file for OpenRC
This in an init file for OpenRC systems.

It should be equivalent in features to the current systemd file, with 2 deliberate changes:
- removed "no-log-dates", as the logs are fine as-is
- Lower nofile limit, as it seems largely sufficient for a self-hosted instance. Feel free to increase to 8192 or 10240 if necessary.

Confirmed functional with Gentoo amd64 and ntfy 2.17.0.
2026-03-11 21:56:14 +01:00
17 changed files with 374 additions and 120 deletions

View File

@@ -146,11 +146,13 @@ web-build:
../server/site/config.js
web-deps:
cd web && $(NPM) install
cd web && $(NPM) ci
# Use "npm ci" so that we don't change the package lock file
# If this fails for .svg files, optimize them with svgo
web-deps-update:
cd web && $(NPM) update
cd web && $(NPM) install
web-fmt:
cd web && $(NPM) run format

View File

@@ -30,6 +30,7 @@ type Store struct {
attachmentsWithSizes func() (map[string]int64, error) // Returns file ID -> size for active attachments
orphanGracePeriod time.Duration // Don't delete orphaned objects younger than this
closeChan chan struct{}
doneChan chan struct{}
mu sync.RWMutex // Protects size and sizes
}
@@ -44,7 +45,7 @@ func NewFileStore(dir string, totalSizeLimit int64, orphanGracePeriod time.Durat
// NewS3Store creates a new S3-backed attachment cache. The s3URL must be in the format:
//
// s3://ACCESS_KEY:SECRET_KEY@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT]
// s3://ACCESS_KEY:SECRET_KEY@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT][&disable_http2=true]
func NewS3Store(s3URL string, totalSizeLimit int64, orphanGracePeriod time.Duration, attachmentsWithSizes func() (map[string]int64, error)) (*Store, error) {
config, err := s3.ParseURL(s3URL)
if err != nil {
@@ -61,6 +62,7 @@ func newStore(backend backend, totalSizeLimit int64, orphanGracePeriod time.Dura
attachmentsWithSizes: attachmentsWithSizes,
orphanGracePeriod: orphanGracePeriod,
closeChan: make(chan struct{}),
doneChan: make(chan struct{}),
}
// Hydrate sizes from the database immediately so that Size()/Remaining()/Remove()
// are accurate from the start, without waiting for the first sync() call.
@@ -74,6 +76,8 @@ func newStore(backend backend, totalSizeLimit int64, orphanGracePeriod time.Dura
c.size += size
}
go c.syncLoop()
} else {
close(c.doneChan)
}
return c, nil
}
@@ -216,12 +220,14 @@ func (c *Store) Remaining() int64 {
return remaining
}
// Close stops the background sync goroutine
// Close stops the background sync goroutine and waits for it to finish
func (c *Store) Close() {
close(c.closeChan)
<-c.doneChan
}
func (c *Store) syncLoop() {
defer close(c.doneChan)
if err := c.sync(); err != nil {
log.Tag(tagStore).Err(err).Warn("Attachment sync failed")
}

View File

@@ -538,7 +538,7 @@ As an alternative to the local filesystem, you can store attachments in an S3-co
To use an S3-compatible storage for attachments, set `attachment-cache-dir` to an S3 URL with the following format:
```
s3://ACCESS_KEY:SECRET_KEY@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT]
s3://ACCESS_KEY:SECRET_KEY@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT][&disable_http2=true]
```
Here are a few examples:
@@ -546,7 +546,7 @@ Here are a few examples:
=== "/etc/ntfy/server.yml (DigitalOcean Spaces)"
``` yaml
base-url: "https://ntfy.example.com"
attachment-cache-dir: "s3://ACCESS_KEY:SECRET_KEY@my-bucket/attachments?region=nyc3&endpoint=https://nyc3.digitaloceanspaces.com"
attachment-cache-dir: "s3://ACCESS_KEY:SECRET_KEY@my-bucket/attachments?region=nyc3&endpoint=https://nyc3.digitaloceanspaces.com&disable_http2=true"
```
=== "/etc/ntfy/server.yml (AWS S3)"
@@ -564,6 +564,9 @@ Here are a few examples:
Note that the access key and secret key may have to be URL encoded. For instance, a secret key `YmxhY+mxhYmxhC` (note the `+`) should
be encoded as `YmxhY%2BmxhYmxhC` (note the `%2B`), so the URL would be `s3://ACCESS_KEY:YmxhY%2BmxhYmxhC@my-bucket/attachments...`.
If you experience upload failures with HTTP/2 stream errors (common with DigitalOcean Spaces and some other S3-compatible providers),
add `&disable_http2=true` to force HTTP/1.1 connections.
!!! info
ntfy.sh is hosted and sponsored by DigitalOcean. I can highly recommend their public cloud offering. It's been rock solid
for 4 years. They offer an S3-compatible storage for $5/month and 250 GB of storage, with 1 TiB of bandwidth.
@@ -2189,7 +2192,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting) |
| `proxy-forwarded-header` | `NTFY_PROXY_FORWARDED_HEADER` | *string* | `X-Forwarded-For` | Use specified header to determine visitor IP address (for rate limiting) |
| `proxy-trusted-hosts` | `NTFY_PROXY_TRUSTED_HOSTS` | *comma-separated host/IP/CIDR list* | - | Comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header |
| `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory or S3 URL* | - | Cache directory for attached files, or S3 URL for object storage (format: `s3://KEY:SECRET@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT]`). |
| `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory or S3 URL* | - | Cache directory for attached files, or S3 URL for object storage (format: `s3://KEY:SECRET@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT][&disable_http2=true]`). |
| `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. |
| `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. |
| `attachment-expiry-duration` | `NTFY_ATTACHMENT_EXPIRY_DURATION` | *duration* | 3h | Duration after which uploaded attachments will be deleted (e.g. 3h, 20h). Strongly affects `visitor-attachment-total-size-limit`. |
@@ -2291,7 +2294,7 @@ OPTIONS:
--auth-file value, --auth_file value, -H value auth database file used for access control [$NTFY_AUTH_FILE]
--auth-startup-queries value, --auth_startup_queries value queries run when the auth database is initialized [$NTFY_AUTH_STARTUP_QUERIES]
--auth-default-access value, --auth_default_access value, -p value default permissions if no matching entries in the auth database are found (default: "read-write") [$NTFY_AUTH_DEFAULT_ACCESS]
--attachment-cache-dir value, --attachment_cache_dir value cache directory for attached files, or S3 URL (s3://ACCESS_KEY:SECRET_KEY@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT]) [$NTFY_ATTACHMENT_CACHE_DIR]
--attachment-cache-dir value, --attachment_cache_dir value cache directory for attached files, or S3 URL (s3://ACCESS_KEY:SECRET_KEY@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT][&disable_http2=true]) [$NTFY_ATTACHMENT_CACHE_DIR]
--attachment-total-size-limit value, --attachment_total_size_limit value, -A value limit of the on-disk attachment cache (default: "5G") [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT]
--attachment-file-size-limit value, --attachment_file_size_limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: "15M") [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT]
--attachment-expiry-duration value, --attachment_expiry_duration value, -X value duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: "3h") [$NTFY_ATTACHMENT_EXPIRY_DURATION]

View File

@@ -28,42 +28,130 @@ resources to get started. _I am not affiliated with Kris or Alex, I just liked t
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
deb/rpm packages.
### Download and run
The steps below allow you to download ntfy server and run it in a pinch. But it won't be enough to install it permanently
as a service starting at boot time.
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_linux_amd64.tar.gz
tar zxvf ntfy_2.19.2_linux_amd64.tar.gz
sudo cp -a ntfy_2.19.2_linux_amd64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.19.2_linux_amd64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_amd64.tar.gz
tar zxvf ntfy_2.20.1_linux_amd64.tar.gz
sudo cp -a ntfy_2.20.1_linux_amd64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.20.1_linux_amd64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_linux_armv6.tar.gz
tar zxvf ntfy_2.19.2_linux_armv6.tar.gz
sudo cp -a ntfy_2.19.2_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.19.2_linux_armv6/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_armv6.tar.gz
tar zxvf ntfy_2.20.1_linux_armv6.tar.gz
sudo cp -a ntfy_2.20.1_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.20.1_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_linux_armv7.tar.gz
tar zxvf ntfy_2.19.2_linux_armv7.tar.gz
sudo cp -a ntfy_2.19.2_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.19.2_linux_armv7/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_armv7.tar.gz
tar zxvf ntfy_2.20.1_linux_armv7.tar.gz
sudo cp -a ntfy_2.20.1_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.20.1_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_linux_arm64.tar.gz
tar zxvf ntfy_2.19.2_linux_arm64.tar.gz
sudo cp -a ntfy_2.19.2_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.19.2_linux_arm64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_arm64.tar.gz
tar zxvf ntfy_2.20.1_linux_arm64.tar.gz
sudo cp -a ntfy_2.20.1_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.20.1_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
### Install as a service
If you want to install ntfy server permanently as a service, and your OS/distribution of choice doesn't offer a package,
there are a few more steps to follow.
Create the ntfy user and group:
```bash
useradd --system --home-dir /var/lib/ntfy --shell /bin/false --comment "User for the simple HTTP-based pub-sub notification service" ntfy
```
Depending on your init system, the following steps will diverge.
#### On systemd systems
Install the ntfy server unit file (which contains parameters to start the service at boot time):
=== "x86_64/amd64"
```bash
sudo mv ntfy_2.20.1_linux_amd64/server/ntfy.service /etc/systemd/system/
sudo chmod 644 /etc/systemd/system/ntfy.service
```
=== "armv6"
```bash
sudo mv ntfy_2.20.1_linux_armv6/server/ntfy.service /etc/systemd/system/
sudo chmod 644 /etc/systemd/system/ntfy.service
```
=== "armv7/armhf"
```bash
sudo mv ntfy_2.20.1_linux_armv7/server/ntfy.service /etc/systemd/system/
sudo chmod 644 /etc/systemd/system/ntfy.service
```
=== "arm64"
```bash
sudo mv ntfy_2.20.1_linux_arm64/server/ntfy.service /etc/systemd/system/
sudo chmod 644 /etc/systemd/system/ntfy.service
```
Then notify systemd we have added a new service and start the service:
```bash
sudo systemctl daemon-reload
sudo systemctl start ntfy
```
#### On OpenRC systems
Install the ntfy server service script:
=== "x86_64/amd64"
```bash
sudo mv ntfy_2.20.1_linux_amd64/server/ntfy.openrc /etc/init.d/ntfy
sudo chmod 755 /etc/init.d/ntfy
```
=== "armv6"
```bash
sudo mv ntfy_2.20.1_linux_armv6/server/ntfy.openrc /etc/init.d/ntfy
sudo chmod 755 /etc/init.d/ntfy
```
=== "armv7/armhf"
```bash
sudo mv ntfy_2.20.1_linux_armv7/server/ntfy.openrc /etc/init.d/ntfy
sudo chmod 755 /etc/init.d/ntfy
```
=== "arm64"
```bash
sudo mv ntfy_2.20.1_linux_arm64/server/ntfy.openrc /etc/init.d/ntfy
sudo chmod 755 /etc/init.d/ntfy
```
Start the ntfy server service:
```bash
sudo rc-service ntfy start
```
Add the ntfy server service to the default runlevel (so that it starts at boot time):
```bash
sudo rc-update add ntfy default
```
## Debian/Ubuntu repository
!!! info
@@ -116,7 +204,7 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_linux_amd64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -124,7 +212,7 @@ Manually installing the .deb file:
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_linux_armv6.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -132,7 +220,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_linux_armv7.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -140,7 +228,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_linux_arm64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -150,28 +238,28 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_linux_amd64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_linux_armv6.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_armv6.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_linux_armv7.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_linux_arm64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
@@ -213,18 +301,18 @@ pkg install go-ntfy
## macOS
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_darwin_all.tar.gz),
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_darwin_all.tar.gz),
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
```bash
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_darwin_all.tar.gz > ntfy_2.19.2_darwin_all.tar.gz
tar zxvf ntfy_2.19.2_darwin_all.tar.gz
sudo cp -a ntfy_2.19.2_darwin_all/ntfy /usr/local/bin/ntfy
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_darwin_all.tar.gz > ntfy_2.20.1_darwin_all.tar.gz
tar zxvf ntfy_2.20.1_darwin_all.tar.gz
sudo cp -a ntfy_2.20.1_darwin_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy
cp ntfy_2.19.2_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
cp ntfy_2.20.1_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help
```
@@ -245,7 +333,7 @@ brew install ntfy
The ntfy server and CLI are fully supported on Windows. You can run the ntfy server directly or as a Windows service.
To install, you can either
* [Download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.19.2/ntfy_2.19.2_windows_amd64.zip),
* [Download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.20.1/ntfy_2.20.1_windows_amd64.zip),
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
* Or install ntfy from the [Scoop](https://scoop.sh) main repository via `scoop install ntfy`

View File

@@ -6,12 +6,46 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
| Component | Version | Release date |
|------------------|---------|--------------|
| ntfy server | v2.19.2 | Mar 16, 2026 |
| ntfy server | v2.20.1 | Mar 27, 2026 |
| ntfy Android app | v1.24.0 | Mar 5, 2026 |
| ntfy iOS app | v1.3 | Nov 26, 2023 |
Please check out the release notes for [upcoming releases](#not-released-yet) below.
### ntfy server v2.20.1
Released March 27, 2026
This is a small bugfix release that only affects high volume S3 backends that struggle with HTTP/2.
**Bug fixes + maintenance:**
* [Attachments](config.md#attachments): Add `disable_http2=true` S3 URL option to work around HTTP/2 stream errors with DigitalOcean Spaces and other S3-compatible providers ([#1678](https://github.com/binwiederhier/ntfy/issues/1678)/[#1679](https://github.com/binwiederhier/ntfy/pull/1679))
### ntfy server v2.20.0
Released March 26, 2026
This release is another step towards making it possible to help scale ntfy up and out 🔥! With this release, you can store
attachments in an S3-compatible object store as an alterative to the directory. See [attachment store](config.md#attachments)
for details.
!!! warning
With this release, ntfy will take full control over the attachment directory or S3 bucket. Files/objects in the configured `attachment-cache-dir`
that match the message ID format (12 chars, matching `^[A-Za-z0-9]{12}$`), and have no entries in the message database will be deleted.
**Do not use a directory or S3 bucket as `attachment-cache-dir` that is also used for something else.**
This is a small behavioral change that was necessary because the old logic often left attachments behind and would not clean them
up. Unless you have re-used the attachment directory for anything else (which is hopefully never done), this should not affect
you at all.
**Features:**
* Add S3-compatible object storage as an alternative [attachment store](config.md#attachments) via `attachment-cache-dir` config option ([#1656](https://github.com/binwiederhier/ntfy/pull/1656)/[#1672](https://github.com/binwiederhier/ntfy/pull/1672))
**Bug fixes + maintenance:**
* Reject invalid e-mail addresses (e.g. multiple comma-separated recipients) with HTTP 400
* Add OpenRC init service file ([#1650](https://github.com/binwiederhier/ntfy/pull/1650), thanks to [@ageru](https://github.com/ageru) for the contribution)
## ntfy server v2.19.2
Released March 16, 2026
@@ -1798,25 +1832,4 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
## Not released yet
### ntfy server v2.20.x (UNRELEASED)
This release is another step towards making it possible to help scale ntfy up and out 🔥! With this release, you can store
attachments in an S3-compatible object store as an alterative to the directory. See [attachment store](config.md#attachments)
for details.
!!! warning
With this release, ntfy will take full control over the attachment directory or S3 bucket. Files/objects in the configured `attachment-cache-dir`
that match the message ID format (12 chars, matching `^[A-Za-z0-9]{12}$`), and have no entries in the message database will be deleted.
**Do not use a directory or S3 bucket as `attachment-cache-dir` that is also used for something else.**
This is a small behavioral change that was necessary because the old logic often left attachments behind and would not clean them
up. Unless you have re-used the attachment directory for anything else (which is hopefully never done), this should not affect
you at all.
**Features:**
* Add S3-compatible object storage as an alternative [attachment store](config.md#attachments) via `attachment-cache-dir` config option ([#1656](https://github.com/binwiederhier/ntfy/pull/1656)/[#1672](https://github.com/binwiederhier/ntfy/pull/1672))
**Bug fixes + maintenance:**
* Reject invalid e-mail addresses (e.g. multiple comma-separated recipients) with HTTP 400
_Nothing._

10
go.mod
View File

@@ -19,7 +19,7 @@ require (
golang.org/x/sync v0.20.0
golang.org/x/term v0.41.0
golang.org/x/time v0.15.0
google.golang.org/api v0.272.0
google.golang.org/api v0.273.0
gopkg.in/yaml.v2 v2.4.0
)
@@ -30,7 +30,7 @@ require github.com/pkg/errors v0.9.1 // indirect
require (
firebase.google.com/go/v4 v4.19.0
github.com/SherClockHolmes/webpush-go v1.4.0
github.com/jackc/pgx/v5 v5.9.0
github.com/jackc/pgx/v5 v5.9.1
github.com/microcosm-cc/bluemonday v1.0.27
github.com/prometheus/client_golang v1.23.2
github.com/stripe/stripe-go/v74 v74.30.0
@@ -41,10 +41,10 @@ require (
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.3-0.20260310051336-87cdcc9f7568 // indirect
cloud.google.com/go/auth v0.19.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/iam v1.6.0 // indirect
cloud.google.com/go/longrunning v0.8.0 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
@@ -70,7 +70,7 @@ require (
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.19.0 // indirect
github.com/googleapis/gax-go/v2 v2.20.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect

20
go.sum
View File

@@ -2,16 +2,16 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.3-0.20260310051336-87cdcc9f7568 h1:PJt3KrySfZkKdcEV2wlyNkfAPbMZGjtnv5oLrT4tWPg=
cloud.google.com/go/auth v0.18.3-0.20260310051336-87cdcc9f7568/go.mod h1:/Tt0rLCp4FHXEBtdyYqvIZPcJzbpJ/fmqtgIaXseDK4=
cloud.google.com/go/auth v0.19.0 h1:DGYwtbcsGsT1ywuxsIoWi1u/vlks0moIblQHgSDgQkQ=
cloud.google.com/go/auth v0.19.0/go.mod h1:2Aph7BT2KnaSFOM0JDPyiYgNh6PL9vGMiP8CUIXZ+IY=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/firestore v1.21.0 h1:BhopUsx7kh6NFx77ccRsHhrtkbJUmDAxNY3uapWdjcM=
cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/iam v1.6.0 h1:JiSIcEi38dWBKhB3BtfKCW+dMvCZJEhBA2BsaGJgoxs=
cloud.google.com/go/iam v1.6.0/go.mod h1:ZS6zEy7QHmcNO18mjO2viYv/n+wOUkhJqGNkPPGueGU=
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
@@ -98,8 +98,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=
github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
github.com/googleapis/gax-go/v2 v2.20.0 h1:NIKVuLhDlIV74muWlsMM4CcQZqN6JJ20Qcxd9YMuYcs=
github.com/googleapis/gax-go/v2 v2.20.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -108,8 +108,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.0 h1:T/dI+2TvmI2H8s/KH1/lXIbz1CUFk3gn5oTjr0/mBsE=
github.com/jackc/pgx/v5 v5.9.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -272,8 +272,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
google.golang.org/api v0.273.0 h1:r/Bcv36Xa/te1ugaN1kdJ5LoA5Wj/cL+a4gj6FiPBjQ=
google.golang.org/api v0.273.0/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew=
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"crypto/md5" //nolint:gosec // MD5 is required by the S3 protocol for Content-MD5 headers
"crypto/tls"
"encoding/base64"
"encoding/xml"
"errors"
@@ -61,7 +62,11 @@ type Client struct {
func New(config *Config) *Client {
httpClient := config.HTTPClient
if httpClient == nil {
httpClient = http.DefaultClient
if config.DisableHTTP2 {
httpClient = newHTTP1Client()
} else {
httpClient = http.DefaultClient
}
}
return &Client{
config: config,
@@ -300,3 +305,20 @@ func (c *Client) do(ctx context.Context, op, method, reqURL string, body []byte,
}
return respBody, nil
}
// newHTTP1Client creates an HTTP client that forces HTTP/1.1 by disabling HTTP/2
// ALPN negotiation. This works around HTTP/2 stream errors with some S3-compatible
// providers (e.g. DigitalOcean Spaces) that can cause non-retryable failures on
// streaming uploads when the server resets the stream mid-transfer.
// See https://github.com/rclone/rclone/issues/4673, https://github.com/golang/go/issues/42777
func newHTTP1Client() *http.Client {
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
ForceAttemptHTTP2: false,
TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper),
},
}
}

View File

@@ -92,6 +92,18 @@ func TestParseURL_EmptyBucket(t *testing.T) {
require.Contains(t, err.Error(), "bucket")
}
func TestParseURL_DisableHTTP2(t *testing.T) {
cfg, err := ParseURL("s3://AKID:SECRET@my-bucket?region=us-east-1&disable_http2=true")
require.Nil(t, err)
require.True(t, cfg.DisableHTTP2)
}
func TestParseURL_DisableHTTP2_NotSet(t *testing.T) {
cfg, err := ParseURL("s3://AKID:SECRET@my-bucket?region=us-east-1")
require.Nil(t, err)
require.False(t, cfg.DisableHTTP2)
}
// --- Unit tests: URL construction ---
func TestConfig_BucketURL_PathStyle(t *testing.T) {

View File

@@ -11,14 +11,15 @@ import (
// Config holds the parsed fields from an S3 URL. Use ParseURL to create one from a URL string.
type Config struct {
Endpoint string // host[:port] only, e.g. "s3.us-east-1.amazonaws.com"
PathStyle bool
Bucket string
Prefix string
Region string
AccessKey string
SecretKey string
HTTPClient *http.Client // if nil, http.DefaultClient is used
Endpoint string // host[:port] only, e.g. "s3.us-east-1.amazonaws.com"
PathStyle bool
Bucket string
Prefix string
Region string
AccessKey string
SecretKey string
DisableHTTP2 bool // Force HTTP/1.1 to work around HTTP/2 issues with some S3-compatible providers
HTTPClient *http.Client // if nil, a default client is created (respecting DisableHTTP2)
}
// BucketURL returns the base URL for bucket-level operations.

View File

@@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"sort"
"strconv"
"strings"
)
@@ -41,9 +42,11 @@ const (
// ParseURL parses an S3 URL of the form:
//
// s3://ACCESS_KEY:SECRET_KEY@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT]
// s3://ACCESS_KEY:SECRET_KEY@BUCKET[/PREFIX]?region=REGION[&endpoint=ENDPOINT][&disable_http2=true]
//
// When endpoint is specified, path-style addressing is enabled automatically.
// When disable_http2=true is set, the client forces HTTP/1.1 to work around
// HTTP/2 stream errors with some S3-compatible providers (e.g. DigitalOcean Spaces).
func ParseURL(s3URL string) (*Config, error) {
u, err := url.Parse(s3URL)
if err != nil {
@@ -80,14 +83,16 @@ func ParseURL(s3URL string) (*Config, error) {
endpoint = fmt.Sprintf("s3.%s.amazonaws.com", region)
pathStyle = false
}
disableHTTP2, _ := strconv.ParseBool(u.Query().Get("disable_http2"))
return &Config{
Endpoint: endpoint,
PathStyle: pathStyle,
Bucket: bucket,
Prefix: prefix,
Region: region,
AccessKey: accessKey,
SecretKey: secretKey,
Endpoint: endpoint,
PathStyle: pathStyle,
Bucket: bucket,
Prefix: prefix,
Region: region,
AccessKey: accessKey,
SecretKey: secretKey,
DisableHTTP2: disableHTTP2,
}, nil
}

49
server/ntfy.openrc Normal file
View File

@@ -0,0 +1,49 @@
#!/sbin/openrc-run
# OpenRC service configuration for ntfy Server.
# Should be placed in /etc/init.d/ as "ntfy" or "ntfy-server" (no extension), owned by root:root and with permissions 755.
# Assumes an ntfy system user and group have been created, for example using this command:
# useradd --system --home-dir /var/lib/ntfy --shell /bin/false --comment "User for the simple HTTP-based pub-sub notification service" ntfy
name=$RC_SVCNAME
description="ntfy server"
command="/usr/local/bin/ntfy"
command_background=true
command_args="serve"
command_user="ntfy:ntfy"
extra_started_commands="reload"
pidfile="/run/${RC_SVCNAME}/${RC_SVCNAME}.pid"
# Changes the hard number of open files (nofile) limit to 2048 for the service.
rc_ulimit="-n 2048"
# Allows the service to bind to privileged ports (<1024).
capabilities="^cap_net_bind_service"
error_log="/var/log/ntfy.log"
# Service dependencies
depend() {
use net
after firewall
}
# Check for - and if necessary - create required files and folders. Might require some adjustment dependings on the content of the server.yml file.
start_pre() {
checkpath -f --owner "$command_user" --mode 0644 \
/var/log/ntfy.log
checkpath -d --owner "$command_user" --mode 0750 \
/run/ntfy/
checkpath -d --owner "$command_user" --mode 0755 \
/var/lib/ntfy/
checkpath -d --owner "$command_user" --mode 0750 \
/var/cache/ntfy/
}
reload() {
ebegin "Reloading $RC_SVCNAME's configuration"
start-stop-daemon --signal SIGHUP --pidfile "${pidfile}"
eend $? "Failed to reload $RC_SVCNAME's configuration"
}

View File

@@ -433,14 +433,18 @@ func (s *Server) Stop() {
s.attachment.Close()
}
s.closeDatabases()
close(s.closeChan)
if s.closeChan != nil {
close(s.closeChan)
}
}
func (s *Server) closeDatabases() {
if s.userManager != nil {
s.userManager.Close()
}
s.messageCache.Close()
if s.messageCache != nil {
s.messageCache.Close()
}
if s.webPush != nil {
s.webPush.Close()
}

View File

@@ -9,6 +9,7 @@ import (
"heckel.io/ntfy/v2/util"
"io"
"net/netip"
"os"
"path/filepath"
"strings"
"testing"
@@ -673,7 +674,6 @@ func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) {
t.Parallel()
conf := newTestConfigWithAuthFile(t, databaseURL)
conf.AuthDefault = user.PermissionReadWrite
conf.AttachmentOrphanGracePeriod = 0 // For testing: delete orphans immediately
s := newTestServer(t, conf)
// Create user with tier
@@ -741,7 +741,11 @@ func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) {
require.Equal(t, 200, rr.Code)
// Verify that messages and attachments were deleted
// This does not explicitly call the manager!
// This does not explicitly call the manager! We backdate the files so sync's
// grace period doesn't protect them.
past := time.Now().Add(-2 * time.Hour)
os.Chtimes(filepath.Join(s.config.AttachmentCacheDir, m1.ID), past, past)
os.Chtimes(filepath.Join(s.config.AttachmentCacheDir, m2.ID), past, past)
waitFor(t, func() bool {
s.attachment.Sync() // File cleanup is done by sync, not by the manager
ms, err := s.messageCache.Messages("mytopic1", model.SinceAllMessages, false)

View File

@@ -14,6 +14,7 @@ import (
"heckel.io/ntfy/v2/util"
"io"
"net/netip"
"os"
"path/filepath"
"strings"
"sync"
@@ -443,7 +444,6 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
c := newTestConfigWithAuthFile(t, databaseURL)
c.StripeSecretKey = "secret key"
c.StripeWebhookKey = "webhook key"
c.AttachmentOrphanGracePeriod = 0 // For testing: delete orphans immediately
s := newTestServer(t, c)
s.stripe = stripeMock
@@ -544,7 +544,11 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
require.Equal(t, 1, len(r)) // "ztopic" reservation was deleted
require.Equal(t, "atopic", r[0].Topic)
// Verify that messages and attachments were deleted
// Verify that messages and attachments were deleted. We backdate the
// attachment files so sync's grace period doesn't protect them.
past := time.Now().Add(-2 * time.Hour)
os.Chtimes(filepath.Join(s.config.AttachmentCacheDir, a2.ID), past, past)
os.Chtimes(filepath.Join(s.config.AttachmentCacheDir, z2.ID), past, past)
time.Sleep(time.Second)
s.execManager()
s.attachment.Sync() // File cleanup is done by sync, not by the manager

View File

@@ -2285,7 +2285,6 @@ func TestServer_PublishAttachmentAndExpire(t *testing.T) {
c := newTestConfig(t, databaseURL)
c.AttachmentExpiryDuration = time.Millisecond // Hack
c.AttachmentOrphanGracePeriod = 0 // For testing: delete orphans immediately
s := newTestServer(t, c)
// Publish and make sure we can retrieve it
@@ -2300,7 +2299,9 @@ func TestServer_PublishAttachmentAndExpire(t *testing.T) {
require.Equal(t, 200, response.Code)
require.Equal(t, content, response.Body.String())
// Prune and makes sure it's gone
// Prune and makes sure it's gone. We backdate the file so sync's grace
// period doesn't protect it, then run the manager + sync explicitly.
require.Nil(t, os.Chtimes(file, time.Now().Add(-2*time.Hour), time.Now().Add(-2*time.Hour)))
waitFor(t, func() bool {
s.execManager()
s.attachment.Sync() // File cleanup is done by sync, not by the manager
@@ -2413,6 +2414,7 @@ func TestServer_PublishAttachmentWithTierBasedLimits(t *testing.T) {
require.Nil(t, s.userManager.AddTier(&user.Tier{
Code: "test",
MessageLimit: 100,
MessageExpiryDuration: time.Hour,
AttachmentFileSizeLimit: 50_000,
AttachmentTotalSizeLimit: 200_000,
AttachmentExpiryDuration: 30 * time.Second,
@@ -2700,7 +2702,7 @@ func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) {
response := request(t, s, "PUT", "/mytopic", "some body", nil)
m := toMessage(t, response.Body.String())
require.Equal(t, "some body", m.Message)
require.True(t, time.Since(start) < 500*time.Millisecond)
require.True(t, time.Since(start) < 2*time.Second)
log.Info("Done: Publishing message; took %s", time.Since(start).Round(time.Millisecond))
// Wait for all Goroutines
@@ -4193,7 +4195,7 @@ func newTestConfigWithAuthFile(t *testing.T, databaseURL string) *Config {
func newTestServer(t *testing.T, config *Config) *Server {
server, err := New(config)
require.Nil(t, err)
t.Cleanup(server.closeDatabases)
t.Cleanup(server.Stop)
return server
}

75
web/package-lock.json generated
View File

@@ -2829,6 +2829,9 @@
"arm"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2843,6 +2846,9 @@
"arm"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2857,6 +2863,9 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2871,6 +2880,9 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2885,6 +2897,9 @@
"loong64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2899,6 +2914,9 @@
"loong64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2913,6 +2931,9 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2927,6 +2948,9 @@
"ppc64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2941,6 +2965,9 @@
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2955,6 +2982,9 @@
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2969,6 +2999,9 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2983,6 +3016,9 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2997,6 +3033,9 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3642,9 +3681,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.10",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz",
"integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==",
"version": "2.10.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz",
"integrity": "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -3766,9 +3805,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001780",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz",
"integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==",
"version": "1.0.30001781",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz",
"integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==",
"dev": true,
"funding": [
{
@@ -4203,9 +4242,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.321",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz",
"integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==",
"version": "1.5.326",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.326.tgz",
"integrity": "sha512-uRBlUfKKdsXMkiiOurgaybNC10tjrD+skXLEg7NHbm6h0uAoqj3xMb9uue5BfcSCXJ4mcyJMOucI6q55D7p6KQ==",
"dev": true,
"license": "ISC"
},
@@ -7068,9 +7107,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -9202,9 +9241,9 @@
}
},
"node_modules/workbox-build/node_modules/brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9270,9 +9309,9 @@
}
},
"node_modules/workbox-build/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {