Merge branch 'main' into e2e
This commit is contained in:
		
						commit
						78f9d4835e
					
				
					 14 changed files with 146 additions and 44 deletions
				
			
		|  | @ -6,6 +6,7 @@ import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"heckel.io/ntfy/log" | 	"heckel.io/ntfy/log" | ||||||
|  | 	"io/fs" | ||||||
| 	"math" | 	"math" | ||||||
| 	"net" | 	"net" | ||||||
| 	"os" | 	"os" | ||||||
|  | @ -35,6 +36,7 @@ var flagsServe = append( | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-unix", Aliases: []string{"listen_unix", "U"}, EnvVars: []string{"NTFY_LISTEN_UNIX"}, Usage: "listen on unix socket path"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-unix", Aliases: []string{"listen_unix", "U"}, EnvVars: []string{"NTFY_LISTEN_UNIX"}, Usage: "listen on unix socket path"}), | ||||||
|  | 	altsrc.NewIntFlag(&cli.IntFlag{Name: "listen-unix-mode", Aliases: []string{"listen_unix_mode"}, EnvVars: []string{"NTFY_LISTEN_UNIX_MODE"}, DefaultText: "system default", Usage: "file permissions of unix socket, e.g. 0700"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"key_file", "K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"key_file", "K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "cert-file", Aliases: []string{"cert_file", "E"}, EnvVars: []string{"NTFY_CERT_FILE"}, Usage: "certificate file, if listen-https is set"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "cert-file", Aliases: []string{"cert_file", "E"}, EnvVars: []string{"NTFY_CERT_FILE"}, Usage: "certificate file, if listen-https is set"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}), | ||||||
|  | @ -99,6 +101,7 @@ func execServe(c *cli.Context) error { | ||||||
| 	listenHTTP := c.String("listen-http") | 	listenHTTP := c.String("listen-http") | ||||||
| 	listenHTTPS := c.String("listen-https") | 	listenHTTPS := c.String("listen-https") | ||||||
| 	listenUnix := c.String("listen-unix") | 	listenUnix := c.String("listen-unix") | ||||||
|  | 	listenUnixMode := c.Int("listen-unix-mode") | ||||||
| 	keyFile := c.String("key-file") | 	keyFile := c.String("key-file") | ||||||
| 	certFile := c.String("cert-file") | 	certFile := c.String("cert-file") | ||||||
| 	firebaseKeyFile := c.String("firebase-key-file") | 	firebaseKeyFile := c.String("firebase-key-file") | ||||||
|  | @ -219,6 +222,7 @@ func execServe(c *cli.Context) error { | ||||||
| 	conf.ListenHTTP = listenHTTP | 	conf.ListenHTTP = listenHTTP | ||||||
| 	conf.ListenHTTPS = listenHTTPS | 	conf.ListenHTTPS = listenHTTPS | ||||||
| 	conf.ListenUnix = listenUnix | 	conf.ListenUnix = listenUnix | ||||||
|  | 	conf.ListenUnixMode = fs.FileMode(listenUnixMode) | ||||||
| 	conf.KeyFile = keyFile | 	conf.KeyFile = keyFile | ||||||
| 	conf.CertFile = certFile | 	conf.CertFile = certFile | ||||||
| 	conf.FirebaseKeyFile = firebaseKeyFile | 	conf.FirebaseKeyFile = firebaseKeyFile | ||||||
|  |  | ||||||
|  | @ -875,6 +875,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | ||||||
| | `listen-http`                              | `NTFY_LISTEN_HTTP`                              | `[host]:port`                                       | `:80`             | Listen address for the HTTP web server                                                                                                                                                                                          | | | `listen-http`                              | `NTFY_LISTEN_HTTP`                              | `[host]:port`                                       | `:80`             | Listen address for the HTTP web server                                                                                                                                                                                          | | ||||||
| | `listen-https`                             | `NTFY_LISTEN_HTTPS`                             | `[host]:port`                                       | -                 | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`.                                                                                                                               | | | `listen-https`                             | `NTFY_LISTEN_HTTPS`                             | `[host]:port`                                       | -                 | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`.                                                                                                                               | | ||||||
| | `listen-unix`                              | `NTFY_LISTEN_UNIX`                              | *filename*                                          | -                 | Path to a Unix socket to listen on                                                                                                                                                                                              | | | `listen-unix`                              | `NTFY_LISTEN_UNIX`                              | *filename*                                          | -                 | Path to a Unix socket to listen on                                                                                                                                                                                              | | ||||||
|  | | `listen-unix-mode`                         | `NTFY_LISTEN_UNIX_MODE`                         | *file mode*                                         | *system default*  | File mode of the Unix socket, e.g. 0700 or 0777                                                                                                                                                                                 | | ||||||
| | `key-file`                                 | `NTFY_KEY_FILE`                                 | *filename*                                          | -                 | HTTPS/TLS private key file, only used if `listen-https` is set.                                                                                                                                                                 | | | `key-file`                                 | `NTFY_KEY_FILE`                                 | *filename*                                          | -                 | HTTPS/TLS private key file, only used if `listen-https` is set.                                                                                                                                                                 | | ||||||
| | `cert-file`                                | `NTFY_CERT_FILE`                                | *filename*                                          | -                 | HTTPS/TLS certificate file, only used if `listen-https` is set.                                                                                                                                                                 | | | `cert-file`                                | `NTFY_CERT_FILE`                                | *filename*                                          | -                 | HTTPS/TLS certificate file, only used if `listen-https` is set.                                                                                                                                                                 | | ||||||
| | `firebase-key-file`                        | `NTFY_FIREBASE_KEY_FILE`                        | *filename*                                          | -                 | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm).                        | | | `firebase-key-file`                        | `NTFY_FIREBASE_KEY_FILE`                        | *filename*                                          | -                 | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm).                        | | ||||||
|  |  | ||||||
|  | @ -9,14 +9,17 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release | ||||||
| **Features:** | **Features:** | ||||||
| 
 | 
 | ||||||
| * Subscriptions can now have a display name ([#313](https://github.com/binwiederhier/ntfy/issues/313), thanks to [@wunter8](https://github.com/wunter8)) | * Subscriptions can now have a display name ([#313](https://github.com/binwiederhier/ntfy/issues/313), thanks to [@wunter8](https://github.com/wunter8)) | ||||||
| * Polling is now done with since=<id> API, which makes deduping easier ([#165](https://github.com/binwiederhier/ntfy/issues/165)) | * Display name for UnifiedPush subscriptions ([#355](https://github.com/binwiederhier/ntfy/issues/355), thanks to [@wunter8](https://github.com/wunter8)) | ||||||
|  | * Polling is now done with `since=<id>` API, which makes deduping easier ([#165](https://github.com/binwiederhier/ntfy/issues/165)) | ||||||
| * Turned JSON stream deprecation banner into "Use WebSockets" banner (no ticket) | * Turned JSON stream deprecation banner into "Use WebSockets" banner (no ticket) | ||||||
|  | * Move action buttons in notification cards ([#236](https://github.com/binwiederhier/ntfy/issues/236), thanks to [@wunter8](https://github.com/wunter8)) | ||||||
| 
 | 
 | ||||||
| **Bugs:** | **Bugs:** | ||||||
| 
 | 
 | ||||||
| * Long-click selecting of notifications doesn't scroll to the top anymore ([#235](https://github.com/binwiederhier/ntfy/issues/235), thanks to [@wunter8](https://github.com/wunter8)) | * Long-click selecting of notifications doesn't scroll to the top anymore ([#235](https://github.com/binwiederhier/ntfy/issues/235), thanks to [@wunter8](https://github.com/wunter8)) | ||||||
| * Add attachment and click URL extras to MESSAGE_RECEIVED broadcast ([#329](https://github.com/binwiederhier/ntfy/issues/329), thanks to [@wunter8](https://github.com/wunter8)) | * Add attachment and click URL extras to MESSAGE_RECEIVED broadcast ([#329](https://github.com/binwiederhier/ntfy/issues/329), thanks to [@wunter8](https://github.com/wunter8)) | ||||||
| * Accessibility: Clear/choose service URL button in base URL dropdown now has a label ([#292](https://github.com/binwiederhier/ntfy/issues/292), thanks to [@mhameed](https://github.com/mhameed) for reporting) | * Accessibility: Clear/choose service URL button in base URL dropdown now has a label ([#292](https://github.com/binwiederhier/ntfy/issues/292), thanks to [@mhameed](https://github.com/mhameed) for reporting) | ||||||
|  | * Web: Switched "Pop" and "Pop Swoosh" sounds ([#352](https://github.com/binwiederhier/ntfy/issues/352), thanks to [@coma-toast](https://github.com/coma-toast) for reporting) | ||||||
| 
 | 
 | ||||||
| **Additional translations:** | **Additional translations:** | ||||||
| 
 | 
 | ||||||
|  | @ -31,11 +34,13 @@ Thank you to [@wunter8](https://github.com/wunter8) for proactively picking up s | ||||||
| **Features:** | **Features:** | ||||||
| 
 | 
 | ||||||
| * Subscription display name for the web app ([#348](https://github.com/binwiederhier/ntfy/pull/348)) | * Subscription display name for the web app ([#348](https://github.com/binwiederhier/ntfy/pull/348)) | ||||||
|  | * Allow setting socket permissions via `--listen-unix-mode` ([#356](https://github.com/binwiederhier/ntfy/pull/356), thanks to [@koro666](https://github.com/koro666)) | ||||||
| 
 | 
 | ||||||
| **Bugs:** | **Bugs:** | ||||||
| 
 | 
 | ||||||
| * `ntfy user` commands don't work with `auth_file` but works with `auth-file` ([#344](https://github.com/binwiederhier/ntfy/issues/344), thanks to [@Histalek](https://github.com/Histalek) for reporting) | * `ntfy user` commands don't work with `auth_file` but works with `auth-file` ([#344](https://github.com/binwiederhier/ntfy/issues/344), thanks to [@Histalek](https://github.com/Histalek) for reporting) | ||||||
| * Ignore new draft HTTP `Priority` header  ([#351](https://github.com/binwiederhier/ntfy/issues/351), thanks to [@ksurl](https://github.com/ksurl) for reporting) | * Ignore new draft HTTP `Priority` header  ([#351](https://github.com/binwiederhier/ntfy/issues/351), thanks to [@ksurl](https://github.com/ksurl) for reporting) | ||||||
|  | * Delete expired attachments based on mod time instead of DB entry to avoid races (no ticket)  | ||||||
| 
 | 
 | ||||||
| **Documentation:** | **Documentation:** | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| package server | package server | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"io/fs" | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -52,6 +53,7 @@ type Config struct { | ||||||
| 	ListenHTTP                           string | 	ListenHTTP                           string | ||||||
| 	ListenHTTPS                          string | 	ListenHTTPS                          string | ||||||
| 	ListenUnix                           string | 	ListenUnix                           string | ||||||
|  | 	ListenUnixMode                       fs.FileMode | ||||||
| 	KeyFile                              string | 	KeyFile                              string | ||||||
| 	CertFile                             string | 	CertFile                             string | ||||||
| 	FirebaseKeyFile                      string | 	FirebaseKeyFile                      string | ||||||
|  | @ -105,6 +107,7 @@ func NewConfig() *Config { | ||||||
| 		ListenHTTP:                           DefaultListenHTTP, | 		ListenHTTP:                           DefaultListenHTTP, | ||||||
| 		ListenHTTPS:                          "", | 		ListenHTTPS:                          "", | ||||||
| 		ListenUnix:                           "", | 		ListenUnix:                           "", | ||||||
|  | 		ListenUnixMode:                       0, | ||||||
| 		KeyFile:                              "", | 		KeyFile:                              "", | ||||||
| 		CertFile:                             "", | 		CertFile:                             "", | ||||||
| 		FirebaseKeyFile:                      "", | 		FirebaseKeyFile:                      "", | ||||||
|  |  | ||||||
|  | @ -2,16 +2,18 @@ package server | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"fmt" | ||||||
| 	"heckel.io/ntfy/util" | 	"heckel.io/ntfy/util" | ||||||
| 	"io" | 	"io" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"sync" | 	"sync" | ||||||
|  | 	"time" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	fileIDRegex      = regexp.MustCompile(`^[-_A-Za-z0-9]+$`) | 	fileIDRegex      = regexp.MustCompile(fmt.Sprintf(`^[-_A-Za-z0-9]{%d}$`, messageIDLength)) | ||||||
| 	errInvalidFileID = errors.New("invalid file ID") | 	errInvalidFileID = errors.New("invalid file ID") | ||||||
| 	errFileExists    = errors.New("file exists") | 	errFileExists    = errors.New("file exists") | ||||||
| ) | ) | ||||||
|  | @ -88,6 +90,25 @@ func (c *fileCache) Remove(ids ...string) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Expired returns a list of file IDs for expired files | ||||||
|  | func (c *fileCache) Expired(olderThan time.Time) ([]string, error) { | ||||||
|  | 	entries, err := os.ReadDir(c.dir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	var ids []string | ||||||
|  | 	for _, e := range entries { | ||||||
|  | 		info, err := e.Info() | ||||||
|  | 		if err != nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if info.ModTime().Before(olderThan) && fileIDRegex.MatchString(e.Name()) { | ||||||
|  | 			ids = append(ids, e.Name()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return ids, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (c *fileCache) Size() int64 { | func (c *fileCache) Size() int64 { | ||||||
| 	c.mu.Lock() | 	c.mu.Lock() | ||||||
| 	defer c.mu.Unlock() | 	defer c.mu.Unlock() | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import ( | ||||||
| 	"os" | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	"time" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
|  | @ -16,10 +17,10 @@ var ( | ||||||
| 
 | 
 | ||||||
| func TestFileCache_Write_Success(t *testing.T) { | func TestFileCache_Write_Success(t *testing.T) { | ||||||
| 	dir, c := newTestFileCache(t) | 	dir, c := newTestFileCache(t) | ||||||
| 	size, err := c.Write("abc", strings.NewReader("normal file"), util.NewFixedLimiter(999)) | 	size, err := c.Write("abcdefghijkl", strings.NewReader("normal file"), util.NewFixedLimiter(999)) | ||||||
| 	require.Nil(t, err) | 	require.Nil(t, err) | ||||||
| 	require.Equal(t, int64(11), size) | 	require.Equal(t, int64(11), size) | ||||||
| 	require.Equal(t, "normal file", readFile(t, dir+"/abc")) | 	require.Equal(t, "normal file", readFile(t, dir+"/abcdefghijkl")) | ||||||
| 	require.Equal(t, int64(11), c.Size()) | 	require.Equal(t, int64(11), c.Size()) | ||||||
| 	require.Equal(t, int64(10229), c.Remaining()) | 	require.Equal(t, int64(10229), c.Remaining()) | ||||||
| } | } | ||||||
|  | @ -27,18 +28,18 @@ func TestFileCache_Write_Success(t *testing.T) { | ||||||
| func TestFileCache_Write_Remove_Success(t *testing.T) { | func TestFileCache_Write_Remove_Success(t *testing.T) { | ||||||
| 	dir, c := newTestFileCache(t) // max = 10k (10240), each = 1k (1024) | 	dir, c := newTestFileCache(t) // max = 10k (10240), each = 1k (1024) | ||||||
| 	for i := 0; i < 10; i++ {     // 10x999 = 9990 | 	for i := 0; i < 10; i++ {     // 10x999 = 9990 | ||||||
| 		size, err := c.Write(fmt.Sprintf("abc%d", i), bytes.NewReader(make([]byte, 999))) | 		size, err := c.Write(fmt.Sprintf("abcdefghijk%d", i), bytes.NewReader(make([]byte, 999))) | ||||||
| 		require.Nil(t, err) | 		require.Nil(t, err) | ||||||
| 		require.Equal(t, int64(999), size) | 		require.Equal(t, int64(999), size) | ||||||
| 	} | 	} | ||||||
| 	require.Equal(t, int64(9990), c.Size()) | 	require.Equal(t, int64(9990), c.Size()) | ||||||
| 	require.Equal(t, int64(250), c.Remaining()) | 	require.Equal(t, int64(250), c.Remaining()) | ||||||
| 	require.FileExists(t, dir+"/abc1") | 	require.FileExists(t, dir+"/abcdefghijk1") | ||||||
| 	require.FileExists(t, dir+"/abc5") | 	require.FileExists(t, dir+"/abcdefghijk5") | ||||||
| 
 | 
 | ||||||
| 	require.Nil(t, c.Remove("abc1", "abc5")) | 	require.Nil(t, c.Remove("abcdefghijk1", "abcdefghijk5")) | ||||||
| 	require.NoFileExists(t, dir+"/abc1") | 	require.NoFileExists(t, dir+"/abcdefghijk1") | ||||||
| 	require.NoFileExists(t, dir+"/abc5") | 	require.NoFileExists(t, dir+"/abcdefghijk5") | ||||||
| 	require.Equal(t, int64(7992), c.Size()) | 	require.Equal(t, int64(7992), c.Size()) | ||||||
| 	require.Equal(t, int64(2248), c.Remaining()) | 	require.Equal(t, int64(2248), c.Remaining()) | ||||||
| } | } | ||||||
|  | @ -46,27 +47,50 @@ func TestFileCache_Write_Remove_Success(t *testing.T) { | ||||||
| func TestFileCache_Write_FailedTotalSizeLimit(t *testing.T) { | func TestFileCache_Write_FailedTotalSizeLimit(t *testing.T) { | ||||||
| 	dir, c := newTestFileCache(t) | 	dir, c := newTestFileCache(t) | ||||||
| 	for i := 0; i < 10; i++ { | 	for i := 0; i < 10; i++ { | ||||||
| 		size, err := c.Write(fmt.Sprintf("abc%d", i), bytes.NewReader(oneKilobyteArray)) | 		size, err := c.Write(fmt.Sprintf("abcdefghijk%d", i), bytes.NewReader(oneKilobyteArray)) | ||||||
| 		require.Nil(t, err) | 		require.Nil(t, err) | ||||||
| 		require.Equal(t, int64(1024), size) | 		require.Equal(t, int64(1024), size) | ||||||
| 	} | 	} | ||||||
| 	_, err := c.Write("abc11", bytes.NewReader(oneKilobyteArray)) | 	_, err := c.Write("abcdefghijkX", bytes.NewReader(oneKilobyteArray)) | ||||||
| 	require.Equal(t, util.ErrLimitReached, err) | 	require.Equal(t, util.ErrLimitReached, err) | ||||||
| 	require.NoFileExists(t, dir+"/abc11") | 	require.NoFileExists(t, dir+"/abcdefghijkX") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestFileCache_Write_FailedFileSizeLimit(t *testing.T) { | func TestFileCache_Write_FailedFileSizeLimit(t *testing.T) { | ||||||
| 	dir, c := newTestFileCache(t) | 	dir, c := newTestFileCache(t) | ||||||
| 	_, err := c.Write("abc", bytes.NewReader(make([]byte, 1025))) | 	_, err := c.Write("abcdefghijkl", bytes.NewReader(make([]byte, 1025))) | ||||||
| 	require.Equal(t, util.ErrLimitReached, err) | 	require.Equal(t, util.ErrLimitReached, err) | ||||||
| 	require.NoFileExists(t, dir+"/abc") | 	require.NoFileExists(t, dir+"/abcdefghijkl") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestFileCache_Write_FailedAdditionalLimiter(t *testing.T) { | func TestFileCache_Write_FailedAdditionalLimiter(t *testing.T) { | ||||||
| 	dir, c := newTestFileCache(t) | 	dir, c := newTestFileCache(t) | ||||||
| 	_, err := c.Write("abc", bytes.NewReader(make([]byte, 1001)), util.NewFixedLimiter(1000)) | 	_, err := c.Write("abcdefghijkl", bytes.NewReader(make([]byte, 1001)), util.NewFixedLimiter(1000)) | ||||||
| 	require.Equal(t, util.ErrLimitReached, err) | 	require.Equal(t, util.ErrLimitReached, err) | ||||||
| 	require.NoFileExists(t, dir+"/abc") | 	require.NoFileExists(t, dir+"/abcdefghijkl") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestFileCache_RemoveExpired(t *testing.T) { | ||||||
|  | 	dir, c := newTestFileCache(t) | ||||||
|  | 	_, err := c.Write("abcdefghijkl", bytes.NewReader(make([]byte, 1001))) | ||||||
|  | 	require.Nil(t, err) | ||||||
|  | 	_, err = c.Write("notdeleted12", bytes.NewReader(make([]byte, 1001))) | ||||||
|  | 	require.Nil(t, err) | ||||||
|  | 
 | ||||||
|  | 	modTime := time.Now().Add(-1 * 4 * time.Hour) | ||||||
|  | 	require.Nil(t, os.Chtimes(dir+"/abcdefghijkl", modTime, modTime)) | ||||||
|  | 
 | ||||||
|  | 	olderThan := time.Now().Add(-1 * 3 * time.Hour) | ||||||
|  | 	ids, err := c.Expired(olderThan) | ||||||
|  | 	require.Nil(t, err) | ||||||
|  | 	require.Equal(t, []string{"abcdefghijkl"}, ids) | ||||||
|  | 	require.Nil(t, c.Remove(ids...)) | ||||||
|  | 	require.NoFileExists(t, dir+"/abcdefghijkl") | ||||||
|  | 	require.FileExists(t, dir+"/notdeleted12") | ||||||
|  | 
 | ||||||
|  | 	ids, err = c.Expired(olderThan) | ||||||
|  | 	require.Nil(t, err) | ||||||
|  | 	require.Empty(t, ids) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func newTestFileCache(t *testing.T) (dir string, cache *fileCache) { | func newTestFileCache(t *testing.T) (dir string, cache *fileCache) { | ||||||
|  |  | ||||||
|  | @ -85,7 +85,6 @@ const ( | ||||||
| 	selectMessageCountPerTopicQuery = `SELECT topic, COUNT(*) FROM messages GROUP BY topic` | 	selectMessageCountPerTopicQuery = `SELECT topic, COUNT(*) FROM messages GROUP BY topic` | ||||||
| 	selectTopicsQuery               = `SELECT topic FROM messages GROUP BY topic` | 	selectTopicsQuery               = `SELECT topic FROM messages GROUP BY topic` | ||||||
| 	selectAttachmentsSizeQuery      = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE sender = ? AND attachment_expires >= ?` | 	selectAttachmentsSizeQuery      = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE sender = ? AND attachment_expires >= ?` | ||||||
| 	selectAttachmentsExpiredQuery   = `SELECT mid FROM messages WHERE attachment_expires > 0 AND attachment_expires < ?` |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Schema management queries | // Schema management queries | ||||||
|  | @ -409,26 +408,6 @@ func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) { | ||||||
| 	return size, nil | 	return size, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *messageCache) AttachmentsExpired() ([]string, error) { |  | ||||||
| 	rows, err := c.db.Query(selectAttachmentsExpiredQuery, time.Now().Unix()) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	defer rows.Close() |  | ||||||
| 	ids := make([]string, 0) |  | ||||||
| 	for rows.Next() { |  | ||||||
| 		var id string |  | ||||||
| 		if err := rows.Scan(&id); err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		ids = append(ids, id) |  | ||||||
| 	} |  | ||||||
| 	if err := rows.Err(); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return ids, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func readMessages(rows *sql.Rows) ([]*message, error) { | func readMessages(rows *sql.Rows) ([]*message, error) { | ||||||
| 	defer rows.Close() | 	defer rows.Close() | ||||||
| 	messages := make([]*message, 0) | 	messages := make([]*message, 0) | ||||||
|  |  | ||||||
|  | @ -344,10 +344,6 @@ func testCacheAttachments(t *testing.T, c *messageCache) { | ||||||
| 	size, err = c.AttachmentBytesUsed("5.6.7.8") | 	size, err = c.AttachmentBytesUsed("5.6.7.8") | ||||||
| 	require.Nil(t, err) | 	require.Nil(t, err) | ||||||
| 	require.Equal(t, int64(0), size) | 	require.Equal(t, int64(0), size) | ||||||
| 
 |  | ||||||
| 	ids, err := c.AttachmentsExpired() |  | ||||||
| 	require.Nil(t, err) |  | ||||||
| 	require.Equal(t, []string{"m1"}, ids) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestSqliteCache_Migration_From0(t *testing.T) { | func TestSqliteCache_Migration_From0(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -204,9 +204,18 @@ func (s *Server) Run() error { | ||||||
| 			os.Remove(s.config.ListenUnix) | 			os.Remove(s.config.ListenUnix) | ||||||
| 			s.unixListener, err = net.Listen("unix", s.config.ListenUnix) | 			s.unixListener, err = net.Listen("unix", s.config.ListenUnix) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
|  | 				s.mu.Unlock() | ||||||
| 				errChan <- err | 				errChan <- err | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  | 			defer s.unixListener.Close() | ||||||
|  | 			if s.config.ListenUnixMode > 0 { | ||||||
|  | 				if err := os.Chmod(s.config.ListenUnix, s.config.ListenUnixMode); err != nil { | ||||||
|  | 					s.mu.Unlock() | ||||||
|  | 					errChan <- err | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 			s.mu.Unlock() | 			s.mu.Unlock() | ||||||
| 			httpServer := &http.Server{Handler: mux} | 			httpServer := &http.Server{Handler: mux} | ||||||
| 			errChan <- httpServer.Serve(s.unixListener) | 			errChan <- httpServer.Serve(s.unixListener) | ||||||
|  | @ -1107,8 +1116,9 @@ func (s *Server) updateStatsAndPrune() { | ||||||
| 	log.Debug("Manager: Deleted %d stale visitor(s)", staleVisitors) | 	log.Debug("Manager: Deleted %d stale visitor(s)", staleVisitors) | ||||||
| 
 | 
 | ||||||
| 	// Delete expired attachments | 	// Delete expired attachments | ||||||
| 	if s.fileCache != nil { | 	if s.fileCache != nil && s.config.AttachmentExpiryDuration > 0 { | ||||||
| 		ids, err := s.messageCache.AttachmentsExpired() | 		olderThan := time.Now().Add(-1 * s.config.AttachmentExpiryDuration) | ||||||
|  | 		ids, err := s.fileCache.Expired(olderThan) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Warn("Error retrieving expired attachments: %s", err.Error()) | 			log.Warn("Error retrieving expired attachments: %s", err.Error()) | ||||||
| 		} else if len(ids) > 0 { | 		} else if len(ids) > 0 { | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ | ||||||
| # This can be useful to avoid port issues on local systems, and to simplify permissions. | # This can be useful to avoid port issues on local systems, and to simplify permissions. | ||||||
| # | # | ||||||
| # listen-unix: <socket-path> | # listen-unix: <socket-path> | ||||||
|  | # listen-unix-mode: <linux permissions, e.g. 0700> | ||||||
| 
 | 
 | ||||||
| # Path to the private key & cert file for the HTTPS web server. Not used if "listen-https" is not set. | # Path to the private key & cert file for the HTTPS web server. Not used if "listen-https" is not set. | ||||||
| # | # | ||||||
|  |  | ||||||
|  | @ -66,6 +66,8 @@ func TestServer_PublishWithFirebase(t *testing.T) { | ||||||
| 	msg1 := toMessage(t, response.Body.String()) | 	msg1 := toMessage(t, response.Body.String()) | ||||||
| 	require.NotEmpty(t, msg1.ID) | 	require.NotEmpty(t, msg1.ID) | ||||||
| 	require.Equal(t, "my first message", msg1.Message) | 	require.Equal(t, "my first message", msg1.Message) | ||||||
|  | 
 | ||||||
|  | 	time.Sleep(100 * time.Millisecond) // Firebase publishing happens | ||||||
| 	require.Equal(t, 1, len(sender.Messages())) | 	require.Equal(t, 1, len(sender.Messages())) | ||||||
| 	require.Equal(t, "my first message", sender.Messages()[0].Data["message"]) | 	require.Equal(t, "my first message", sender.Messages()[0].Data["message"]) | ||||||
| 	require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.Aps.Alert.Body) | 	require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.Aps.Alert.Body) | ||||||
|  |  | ||||||
							
								
								
									
										56
									
								
								web/public/static/langs/zh_Hant.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								web/public/static/langs/zh_Hant.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,56 @@ | ||||||
|  | { | ||||||
|  |     "action_bar_logo_alt": "ntfy 標識", | ||||||
|  |     "action_bar_unsubscribe": "取消訂閱", | ||||||
|  |     "action_bar_toggle_mute": "通知靜音/解除通知靜音", | ||||||
|  |     "action_bar_toggle_action_menu": "開啟/關閉操作選單", | ||||||
|  |     "message_bar_type_message": "在這邊輸入訊息", | ||||||
|  |     "alert_grant_description": "允許瀏覽器權限以顯示桌面通知。", | ||||||
|  |     "alert_grant_button": "允許", | ||||||
|  |     "notifications_list": "通知清單", | ||||||
|  |     "notifications_list_item": "通知", | ||||||
|  |     "notifications_mark_read": "標示已讀", | ||||||
|  |     "notifications_attachment_image": "附加圖片", | ||||||
|  |     "notifications_attachment_copy_url_title": "複製附件URL到剪貼板", | ||||||
|  |     "notifications_attachment_copy_url_button": "複製URL", | ||||||
|  |     "notifications_attachment_open_title": "前往 {{url}}", | ||||||
|  |     "notifications_attachment_open_button": "開啟附件", | ||||||
|  |     "notifications_attachment_link_expired": "下載連結已過期", | ||||||
|  |     "notifications_attachment_file_video": "影片檔案", | ||||||
|  |     "notifications_attachment_file_app": "Android 應用程式檔案", | ||||||
|  |     "notifications_attachment_file_document": "其他文件", | ||||||
|  |     "notifications_click_copy_url_title": "複製連結URL到剪貼板", | ||||||
|  |     "notifications_click_copy_url_button": "複製連結", | ||||||
|  |     "notifications_click_open_button": "開啟連結", | ||||||
|  |     "notifications_actions_not_supported": "網頁程式無法支援該動作", | ||||||
|  |     "notifications_actions_http_request_title": "傳送 HTTP {{method}} 到 {{url}}", | ||||||
|  |     "notifications_none_for_topic_title": "尚未收到任何此主題的通知。", | ||||||
|  |     "notifications_none_for_topic_description": "如要寄送通知到此主題,請使用 PUT 或 POST 到此主題URL。", | ||||||
|  |     "notifications_none_for_any_title": "尚未收到任何通知。", | ||||||
|  |     "action_bar_settings": "設定", | ||||||
|  |     "action_bar_send_test_notification": "寄送測試通知", | ||||||
|  |     "action_bar_clear_notifications": "清除所有通知", | ||||||
|  |     "action_bar_show_menu": "顯示選單", | ||||||
|  |     "nav_button_documentation": "文件", | ||||||
|  |     "nav_button_publish_message": "發布通知", | ||||||
|  |     "nav_button_muted": "通知已靜音", | ||||||
|  |     "notifications_copied_to_clipboard": "複製到剪貼板", | ||||||
|  |     "message_bar_publish": "發布訊息", | ||||||
|  |     "message_bar_show_dialog": "顯示發布對話筐", | ||||||
|  |     "message_bar_error_publishing": "無法發布通知", | ||||||
|  |     "nav_topics_title": "訂閱主題", | ||||||
|  |     "nav_button_all_notifications": "所有通知", | ||||||
|  |     "nav_button_settings": "設定", | ||||||
|  |     "nav_button_subscribe": "訂閱主題", | ||||||
|  |     "nav_button_connecting": "連線中", | ||||||
|  |     "alert_grant_title": "通知已關閉", | ||||||
|  |     "alert_not_supported_title": "不支援通知", | ||||||
|  |     "alert_not_supported_description": "瀏覽器不支援通知。", | ||||||
|  |     "notifications_tags": "標籤", | ||||||
|  |     "notifications_priority_x": "優先度 {{priority}}", | ||||||
|  |     "notifications_new_indicator": "新通知", | ||||||
|  |     "notifications_attachment_file_audio": "聲音檔案", | ||||||
|  |     "notifications_delete": "刪除", | ||||||
|  |     "notifications_attachment_link_expires": "連結已過期 {{date}}", | ||||||
|  |     "notifications_attachment_file_image": "圖片檔案", | ||||||
|  |     "notifications_actions_open_url_title": "前往 {{url}}" | ||||||
|  | } | ||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue