Skip to content
Next Next commit
feat(archive): add concurrency control for archive operations
Introduce a new `archiveWorkers` channel to limit the number of concurrent archive operations. When the limit is reached, new requests will receive a 429 Too Many Requests response. This prevents resource exhaustion and improves system stability. The maximum number of workers can be configured via the `--max-archive-workers` CLI option.
  • Loading branch information
OlegChuev-MD authored and marjune163 committed Apr 26, 2025
commit fe017d99c6300d46e4a0735b0ba7214ca668b47c
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Will generate executable file "main" in current directory.
Start server on port 8080, root directory is current working directory:
```sh
ghfs -l 8080
```
```

Start server on port 8080, root directory is /usr/share/doc:
```sh
Expand Down Expand Up @@ -258,7 +258,10 @@ ghfs [options]
-A|--global-archive
Allow user to download the whole contents of current directory for all url paths.
A download link will appear on top part of the page.
Make sure there is no circular symbol links.
--max-archive-workers <number>
Maximum number of concurrent archive operations.
Set to -1 for unlimited (default).
When the limit is reached, new archive requests will receive 429 Too Many Requests.
--archive <url-path> ...
--archive-user <separator><url-path>[<separator><allowed-username>...] ...
Allow user to download the whole contents of current directory for specific url paths(and sub paths).
Expand Down Expand Up @@ -304,7 +307,7 @@ ghfs [options]
-S|--show <wildcard> ...
-SD|--show-dir <wildcard> ...
-SF|--show-file <wildcard> ...
If specified, files or directories match wildcards(except hidden by hide option) will be shown.
If specified, files or directories match wildcards(except hidden by hide option) will be shown.

-H|--hide <wildcard> ...
-HD|--hide-dir <wildcard> ...
Expand Down
14 changes: 10 additions & 4 deletions src/param/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package param

import (
"errors"
"mjpclab.dev/ghfs/src/goNixArgParser"
"mjpclab.dev/ghfs/src/goVirtualHost"
"mjpclab.dev/ghfs/src/serverError"
"net/http"
"os"
"strings"

"mjpclab.dev/ghfs/src/goNixArgParser"
"mjpclab.dev/ghfs/src/goVirtualHost"
"mjpclab.dev/ghfs/src/serverError"
)

var cliCmd = NewCliCmd()
Expand Down Expand Up @@ -147,6 +148,9 @@ func NewCliCmd() *goNixArgParser.Command {
err = options.AddFlagValues("archivedirsusers", "--archive-dir-user", "", nil, "file system path that allow archive files for specific users, <sep><fs-path>[<sep><user>...]")
serverError.CheckFatal(err)

err = options.AddFlagValue("maxarchiveworkers", "--max-archive-workers", "", "-1", "maximum number of concurrent archive operations (-1 for unlimited)")
serverError.CheckFatal(err)

err = options.AddFlag("globalcors", "--global-cors", "GHFS_GLOBAL_CORS", "enable CORS headers for all directories")
serverError.CheckFatal(err)

Expand Down Expand Up @@ -436,6 +440,8 @@ func CmdResultsToParams(results []*goNixArgParser.ParseResult) (params Params, e
archiveDirsUsers, _ := result.GetStrings("archivedirsusers")
param.ArchiveDirsUsers = SplitAllKeyValues(archiveDirsUsers)

param.ArchiveMaxWorkers, _ = result.GetInt("maxarchiveworkers")

// global restrict access
if result.HasKey("globalrestrictaccess") {
param.GlobalRestrictAccess, _ = result.GetStrings("globalrestrictaccess")
Expand Down Expand Up @@ -464,7 +470,7 @@ func CmdResultsToParams(results []*goNixArgParser.ParseResult) (params Params, e
// certificate
certFiles, _ := result.GetStrings("certs")
keyFiles, _ := result.GetStrings("keys")
param.CertKeyPaths, es = goVirtualHost.CertsKeysToPairs(certFiles, keyFiles)
param.CertKeyPaths, _ = goVirtualHost.CertsKeysToPairs(certFiles, keyFiles)

// listen
listens, _ := result.GetStrings("listens")
Expand Down
21 changes: 14 additions & 7 deletions src/param/main.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package param

import (
"os"
"path/filepath"

"mjpclab.dev/ghfs/src/middleware"
"mjpclab.dev/ghfs/src/serverError"
"mjpclab.dev/ghfs/src/util"
"os"
"path/filepath"
)

type Param struct {
Expand Down Expand Up @@ -56,11 +57,13 @@ type Param struct {
DeleteDirs []string
DeleteDirsUsers [][]string // [][path, user...]

GlobalArchive bool
ArchiveUrls []string
ArchiveUrlsUsers [][]string // [][path, user...]
ArchiveDirs []string
ArchiveDirsUsers [][]string // [][path, user...]
GlobalArchive bool
ArchiveUrls []string
ArchiveUrlsUsers [][]string // [][path, user...]
ArchiveDirs []string
ArchiveDirsUsers [][]string // [][path, user...]
ArchiveMaxWorkers int
ArchivationsSem chan struct{}

GlobalCors bool
CorsUrls []string
Expand Down Expand Up @@ -163,6 +166,10 @@ func (param *Param) Normalize() (errs []error) {
param.DeleteDirs = NormalizeFsPaths(param.DeleteDirs)
param.ArchiveUrls = NormalizeUrlPaths(param.ArchiveUrls)
param.ArchiveDirs = NormalizeFsPaths(param.ArchiveDirs)
if param.ArchiveMaxWorkers > 0 {
param.ArchivationsSem = make(chan struct{}, param.ArchiveMaxWorkers)
}

param.CorsUrls = NormalizeUrlPaths(param.CorsUrls)
param.CorsDirs = NormalizeFsPaths(param.CorsDirs)

Expand Down
53 changes: 34 additions & 19 deletions src/serverHandler/aliasHandler.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package serverHandler

import (
"net/http"
"regexp"
"strconv"
"strings"

"mjpclab.dev/ghfs/src/middleware"
"mjpclab.dev/ghfs/src/param"
"mjpclab.dev/ghfs/src/serverLog"
"mjpclab.dev/ghfs/src/tpl/theme"
"mjpclab.dev/ghfs/src/user"
"mjpclab.dev/ghfs/src/util"
"net/http"
"regexp"
"strconv"
"strings"
)

var defaultHandler = http.NotFoundHandler()
Expand Down Expand Up @@ -48,6 +49,8 @@ type aliasHandler struct {
archive *hierarchyAvailability
cors *hierarchyAvailability

archiveWorkers chan struct{}

globalRestrictAccess []string
restrictAccessUrls pathStringsList
restrictAccessDirs pathStringsList
Expand Down Expand Up @@ -120,21 +123,8 @@ func (h *aliasHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

if session.isMutate && h.mutate(w, r, session, data) {
return
} else if session.isArchive {
switch session.archiveFormat {
case tarFmt:
if h.tar(w, r, session, data) {
return
}
case tgzFmt:
if h.tgz(w, r, session, data) {
return
}
case zipFmt:
if h.zip(w, r, session, data) {
return
}
}
} else if session.isArchive && h.createArchive(w, r, session, data) {
return
}
}

Expand All @@ -152,6 +142,29 @@ func (h *aliasHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}

func (h *aliasHandler) createArchive(w http.ResponseWriter, r *http.Request, session *sessionContext, data *responseData) bool {
if h.archiveWorkers != nil {
select {
case h.archiveWorkers <- struct{}{}:
defer func() { <-h.archiveWorkers }()
default:
data.Status = http.StatusTooManyRequests
return false
}
}

switch session.archiveFormat {
case tarFmt:
return h.tar(w, r, session, data)
case tgzFmt:
return h.tgz(w, r, session, data)
case zipFmt:
return h.zip(w, r, session, data)
}

return false
}

func newAliasHandler(
p *param.Param,
vhostCtx *vhostContext,
Expand Down Expand Up @@ -179,6 +192,8 @@ func newAliasHandler(
toHttpsPort: p.ToHttpsPort,
defaultSort: p.DefaultSort,

archiveWorkers: p.ArchivationsSem,

users: vhostCtx.users,
theme: vhostCtx.theme,
logger: vhostCtx.logger,
Expand Down