Jelajahi Sumber

feat(fastdl): incremental, parallel FastDL with custom folders and exclusions (#5100)

* feat(fastdl): incremental, parallel FastDL with custom folders and exclusions

The fastdl command wiped the FastDL directory on every run and then
copied and bzip2-compressed every file again, one at a time. On a stock
TF2 install that is several GB of work each run. It also ignored content
in Source's custom/<name>/ folders and had no way to leave out paths
such as maps/workshop/.

The command now builds one file list, works out what changed and only
touches those files:

- Source content is also collected from custom/<name>/<type>/ using the
  same per-type extension lists, and lands at the path clients request
  (custom/foo/maps/x.bsp goes to maps/x.bsp). When the same path exists
  more than once, custom folders win over the stock folder and the first
  custom folder in name order wins, following the server's own search
  path. Skipped duplicates are logged.
- The FastDL directory is kept between runs. A file is copied and
  compressed only when its FastDL copy is missing or its modification
  time differs from the source. Compressed files get their source's
  mtime with touch -r, because bzip2 only keeps whole seconds. Files in
  FastDL with no source any more are removed, along with empty folders.
- Files over 150 MB are copied without compression, as Source clients
  will not download a larger compressed file.
- Compression runs in parallel with xargs -P "$(nproc)". Each file is
  still logged as a pass or fail.
- New fastdlexclude setting: space-separated paths relative to the
  FastDL directory. Defaults to maps/workshop/ for Source servers and is
  empty for GoldSrc. Excluded files already in FastDL are removed.
- The "Overwrite existing directory?" prompt becomes "Rebuild Fastdl
  directory from scratch?" (default no). Yes clears the directory and
  rebuilds everything as before.

Garry's Mod uses the same file list, mapping addons/<name>/ and lua/
content to the same paths as the previous copy-then-move steps, and the
download enforcer is generated from the client paths in that list.

GoldSrc keeps its directory layout and file types from #5045, copies
without compression, and is now incremental too.

Fixes #1521
Fixes #4138
Fixes #2873
Fixes #3153

* feat(fastdl): only remove files fastdl wrote, tracked in a record

Removing every file in the FastDL directory without a source deleted
files users put there on purpose, such as index.html or .htaccess, and
when two servers shared one FastDL directory each run deleted the other
server's files.

Each run now saves a record of the files it wrote to
lgsm/data/<selfname>-fastdl.txt: the FastDL path, the source path, its
size and its modification time. Only files listed in the previous
record and no longer expected are removed. Anything else in the
directory is counted and reported as left alone. The record starts with
the FastDL directory it belongs to and is ignored if webdir changes.

Change detection also uses the record: a file is updated when its FastDL
copy is missing or its source path, size or modification time differs.
Comparing the modification time alone missed a change of winning source
for a duplicate path, because files written within the same few
milliseconds share a timestamp. Compressed files no longer need their
mtime copied from the source.

The record is saved only after a run completes. The rebuild prompt now
says that it deletes everything in the directory.

* refactor(fastdl): drop explanatory comments and fix log typo

Keep comment density in line with other modules, ignore blank lines in
the file record, and fix "filesr" in the download enforcer log line.
Daniel Gibbs 4 jam lalu
induk
melakukan
034471f150
46 mengubah file dengan 533 tambahan dan 211 penghapusan
  1. 5 0
      lgsm/config-default/config-lgsm/ahl2server/_default.cfg
  2. 5 0
      lgsm/config-default/config-lgsm/ahlserver/_default.cfg
  3. 5 0
      lgsm/config-default/config-lgsm/bb2server/_default.cfg
  4. 5 0
      lgsm/config-default/config-lgsm/bbserver/_default.cfg
  5. 5 0
      lgsm/config-default/config-lgsm/bdserver/_default.cfg
  6. 5 0
      lgsm/config-default/config-lgsm/bmdmserver/_default.cfg
  7. 5 0
      lgsm/config-default/config-lgsm/bsserver/_default.cfg
  8. 5 0
      lgsm/config-default/config-lgsm/ccserver/_default.cfg
  9. 5 0
      lgsm/config-default/config-lgsm/csczserver/_default.cfg
  10. 5 0
      lgsm/config-default/config-lgsm/csgoserver/_default.cfg
  11. 5 0
      lgsm/config-default/config-lgsm/csserver/_default.cfg
  12. 5 0
      lgsm/config-default/config-lgsm/cssserver/_default.cfg
  13. 5 0
      lgsm/config-default/config-lgsm/dabserver/_default.cfg
  14. 5 0
      lgsm/config-default/config-lgsm/dmcserver/_default.cfg
  15. 5 0
      lgsm/config-default/config-lgsm/dodserver/_default.cfg
  16. 5 0
      lgsm/config-default/config-lgsm/dodsserver/_default.cfg
  17. 5 0
      lgsm/config-default/config-lgsm/doiserver/_default.cfg
  18. 5 0
      lgsm/config-default/config-lgsm/dysserver/_default.cfg
  19. 5 0
      lgsm/config-default/config-lgsm/emserver/_default.cfg
  20. 5 0
      lgsm/config-default/config-lgsm/fofserver/_default.cfg
  21. 5 0
      lgsm/config-default/config-lgsm/gmodserver/_default.cfg
  22. 5 0
      lgsm/config-default/config-lgsm/hl2dmserver/_default.cfg
  23. 5 0
      lgsm/config-default/config-lgsm/hldmserver/_default.cfg
  24. 5 0
      lgsm/config-default/config-lgsm/hldmsserver/_default.cfg
  25. 5 0
      lgsm/config-default/config-lgsm/insserver/_default.cfg
  26. 5 0
      lgsm/config-default/config-lgsm/iosserver/_default.cfg
  27. 5 0
      lgsm/config-default/config-lgsm/jbep3server/_default.cfg
  28. 5 0
      lgsm/config-default/config-lgsm/l4d2server/_default.cfg
  29. 5 0
      lgsm/config-default/config-lgsm/l4dserver/_default.cfg
  30. 5 0
      lgsm/config-default/config-lgsm/mcvserver/_default.cfg
  31. 5 0
      lgsm/config-default/config-lgsm/ndserver/_default.cfg
  32. 5 0
      lgsm/config-default/config-lgsm/nmrihserver/_default.cfg
  33. 5 0
      lgsm/config-default/config-lgsm/nsserver/_default.cfg
  34. 5 0
      lgsm/config-default/config-lgsm/opforserver/_default.cfg
  35. 5 0
      lgsm/config-default/config-lgsm/pvkiiserver/_default.cfg
  36. 5 0
      lgsm/config-default/config-lgsm/ricochetserver/_default.cfg
  37. 5 0
      lgsm/config-default/config-lgsm/sfcserver/_default.cfg
  38. 5 0
      lgsm/config-default/config-lgsm/svenserver/_default.cfg
  39. 5 0
      lgsm/config-default/config-lgsm/tf2cserver/_default.cfg
  40. 5 0
      lgsm/config-default/config-lgsm/tf2server/_default.cfg
  41. 5 0
      lgsm/config-default/config-lgsm/tfcserver/_default.cfg
  42. 5 0
      lgsm/config-default/config-lgsm/tsserver/_default.cfg
  43. 5 0
      lgsm/config-default/config-lgsm/vsserver/_default.cfg
  44. 5 0
      lgsm/config-default/config-lgsm/zmrserver/_default.cfg
  45. 5 0
      lgsm/config-default/config-lgsm/zpsserver/_default.cfg
  46. 308 211
      lgsm/modules/command_fastdl.sh

+ 5 - 0
lgsm/config-default/config-lgsm/ahl2server/_default.cfg

@@ -124,6 +124,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/ahlserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/bb2server/_default.cfg

@@ -125,6 +125,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/bbserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/bdserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/bmdmserver/_default.cfg

@@ -125,6 +125,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/bsserver/_default.cfg

@@ -129,6 +129,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/ccserver/_default.cfg

@@ -120,6 +120,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/csczserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/csgoserver/_default.cfg

@@ -157,6 +157,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/csserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/cssserver/_default.cfg

@@ -125,6 +125,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/dabserver/_default.cfg

@@ -120,6 +120,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/dmcserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/dodserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/dodsserver/_default.cfg

@@ -120,6 +120,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/doiserver/_default.cfg

@@ -121,6 +121,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/dysserver/_default.cfg

@@ -125,6 +125,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/emserver/_default.cfg

@@ -125,6 +125,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/fofserver/_default.cfg

@@ -120,6 +120,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/gmodserver/_default.cfg

@@ -130,6 +130,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/hl2dmserver/_default.cfg

@@ -122,6 +122,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/hldmserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/hldmsserver/_default.cfg

@@ -120,6 +120,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/insserver/_default.cfg

@@ -126,6 +126,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/iosserver/_default.cfg

@@ -120,6 +120,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/jbep3server/_default.cfg

@@ -114,6 +114,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/l4d2server/_default.cfg

@@ -125,6 +125,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/l4dserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/mcvserver/_default.cfg

@@ -105,6 +105,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/ndserver/_default.cfg

@@ -125,6 +125,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/nmrihserver/_default.cfg

@@ -125,6 +125,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/nsserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/opforserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/pvkiiserver/_default.cfg

@@ -120,6 +120,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/ricochetserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/sfcserver/_default.cfg

@@ -120,6 +120,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/svenserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/tf2cserver/_default.cfg

@@ -127,6 +127,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/tf2server/_default.cfg

@@ -127,6 +127,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/tfcserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/tsserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/vsserver/_default.cfg

@@ -119,6 +119,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude=""
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/zmrserver/_default.cfg

@@ -120,6 +120,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 5 - 0
lgsm/config-default/config-lgsm/zpsserver/_default.cfg

@@ -125,6 +125,11 @@ maxbackups="4"
 maxbackupdays="30"
 stoponbackup="on"
 
+## Fastdl | https://docs.linuxgsm.com/commands/fastdl
+# Paths to leave out of Fastdl, relative to the Fastdl directory and separated by spaces.
+# e.g. "maps/workshop/ sound/music/"
+fastdlexclude="maps/workshop/"
+
 ## Logging | https://docs.linuxgsm.com/features/logging
 consolelogging="on"
 logdays="7"

+ 308 - 211
lgsm/modules/command_fastdl.sh

@@ -21,6 +21,17 @@ luasvautorundir="${systemdir}/lua/autorun/server"
 luafastdlfile="lgsm_cl_force_fastdl.lua"
 luafastdlfullpath="${luasvautorundir}/${luafastdlfile}"
 
+# File lists.
+fastdlfilelist="${tmpdir}/fastdl_files.txt"
+fastdlduplicatelist="${tmpdir}/fastdl_files_duplicate.txt"
+fastdlnewrecord="${tmpdir}/fastdl_record.txt"
+fastdlrecord="${datadir}/${selfname}-fastdl.txt"
+fastdlclientlist="${tmpdir}/fastdl_files_client.txt"
+fastdlupdatelist="${tmpdir}/fastdl_files_to_update.txt"
+fastdlremovelist="${tmpdir}/fastdl_files_to_remove.txt"
+fastdlcompresslist="${tmpdir}/fastdl_files_to_compress.txt"
+fastdlmaxcompresssize="157286400"
+
 # Header
 fn_print_header
 fn_print_nl "More info: ${italic}https://docs.linuxgsm.com/commands/fastdl"
@@ -30,15 +41,17 @@ fn_print_nl ""
 fn_print_nl "${bold}${lightyellow}${commandaction} Setup"
 fn_messages_separator
 
-# Prompt for clearing old files if directory was already here.
+# Prompt for a full rebuild if directory was already here.
+fastdlrebuild="off"
 if [ -d "${fastdldir}" ]; then
-	fn_print_warning_nl "Fastdl directory already exists."
+	fn_print_info_nl "Fastdl directory already exists, only changed files will be updated."
 	fn_print_nl "${fastdldir}"
 	fn_print_nl ""
-	if fn_prompt_yn "Overwrite existing directory?" Y; then
-		fn_script_log_info "Overwrite existing directory: YES"
+	if fn_prompt_yn "Rebuild Fastdl directory from scratch? This deletes everything in it." N; then
+		fastdlrebuild="on"
+		fn_script_log_info "Rebuild Fastdl directory from scratch: YES"
 	else
-		core_exit.sh
+		fn_script_log_info "Rebuild Fastdl directory from scratch: NO"
 	fi
 fi
 
@@ -50,7 +63,7 @@ if [ "${shortname}" == "gmod" ]; then
 		fn_script_log_info "Force clients to download files: YES"
 	else
 		luaresource="off"
-		fn_script_log_info "Force clients to download filesr: NO"
+		fn_script_log_info "Force clients to download files: NO"
 	fi
 fi
 
@@ -153,34 +166,182 @@ fn_fastdl_goldsrc_extensions() {
 	fi
 }
 
+# Sets the Source file types to include for the current directory.
+fn_fastdl_source_extensions() {
+	fastdl_find_maxdepth=()
+	if [ "${directory}" == "maps" ]; then
+		allowed_extentions_array=("*.bsp" "*.ain" "*.nav" "*.jpg" "*.txt")
+	elif [ "${directory}" == "materials" ]; then
+		allowed_extentions_array=("*.vtf" "*.vmt" "*.vbf" "*.png" "*.svg")
+	elif [ "${directory}" == "models" ]; then
+		allowed_extentions_array=("*.vtx" "*.vvd" "*.mdl" "*.phy" "*.jpg" "*.png" "*.vmt" "*.vtf")
+	elif [ "${directory}" == "particles" ]; then
+		allowed_extentions_array=("*.pcf")
+	elif [ "${directory}" == "sound" ]; then
+		allowed_extentions_array=("*.wav" "*.mp3" "*.ogg")
+	elif [ "${directory}" == "resource" ]; then
+		allowed_extentions_array=("*")
+	fi
+}
+
+# Adds the allowed files in a content directory to the file list.
+fn_fastdl_gather() {
+	local basedir="${1}"
+	local priority="${2}"
+	local label="${directory}"
+	if [ "${basedir}" != "${systemdir}" ]; then
+		label="${basedir#"${systemdir}/"}/${directory}"
+	fi
+	for allowed_extention in "${allowed_extentions_array[@]}"; do
+		fileswc=0
+		tput sc
+		while IFS=$'\t' read -r filesize filemtime fastdlfile; do
+			((fileswc++))
+			tput rc
+			tput el
+			fn_print "gathering ${label} ${allowed_extention} : ${fileswc}"
+			fastdlrelpath="${fastdlfile#"${basedir}/"}"
+			printf '%s\t%s\t%s\t%s\t%s\n' "${priority}" "${fastdlrelpath#./}" "${filesize}" "${filemtime}" "${fastdlfile}" >> "${fastdlfilelist}"
+		done < <(find "${basedir}/${directory}" "${fastdl_find_maxdepth[@]}" -type f -iname "${allowed_extention}" -printf '%s\t%T@\t%p\n')
+		tput rc
+		tput el
+		fn_print "gathering ${label} ${allowed_extention} : ${fileswc}"
+		if [ ${fileswc} != 0 ]; then
+			fn_print_ok_eol_nl
+		else
+			fn_print_info_eol_nl
+		fi
+	done
+}
+
+# Adds Garry's Mod files to the file list.
+fn_fastdl_gather_gmod() {
+	cd "${systemdir}" || exit
+	allowed_extentions_array=("*.ain" "*.bsp" "*.mdl" "*.mp3" "*.ogg" "*.otf" "*.pcf" "*.phy" "*.png" "*.svg" "*.vtf" "*.vmt" "*.vtx" "*.vvd" "*.ttf" "*.wav")
+	for allowed_extention in "${allowed_extentions_array[@]}"; do
+		fileswc=0
+		tput sc
+		while IFS=$'\t' read -r filesize filemtime fastdlfile; do
+			((fileswc++))
+			tput rc
+			tput el
+			fn_print "gathering ${allowed_extention} : ${fileswc}"
+			fastdlrelpath="${fastdlfile#./}"
+			priority="2"
+			if [[ "${fastdlrelpath}" == addons/* ]]; then
+				addonname="${fastdlrelpath#addons/}"
+				addonname="${addonname%%/*}"
+				if [ "addons/${addonname}" == "${fastdlrelpath}" ]; then
+					continue
+				fi
+				priority="1:${addonname}"
+				fastdlrelpath="${fastdlrelpath#addons/"${addonname}"/}"
+			fi
+			printf '%s\t%s\t%s\t%s\t%s\n' "${priority}" "${fastdlrelpath}" "${filesize}" "${filemtime}" "${systemdir}/${fastdlfile#./}" >> "${fastdlfilelist}"
+			if [[ "${fastdlrelpath}" == lua/* ]]; then
+				printf '%s\t%s\t%s\t%s\t%s\n' "0" "${fastdlrelpath#lua/}" "${filesize}" "${filemtime}" "${systemdir}/${fastdlfile#./}" >> "${fastdlfilelist}"
+			fi
+		done < <(find . -type f -iname "${allowed_extention}" -printf '%s\t%T@\t%p\n')
+		if [ ${fileswc} != 0 ]; then
+			fn_print_ok_eol_nl
+		else
+			fn_print_skip_eol_nl
+		fi
+	done
+}
+
+# Works out which files need to be copied, compressed or removed.
+fn_fastdl_plan() {
+	LC_ALL=C sort -t $'\t' -k2,2 -k1,1 "${fastdlfilelist}" | awk -F '\t' -v dupes="${fastdlduplicatelist}" 'seen[$2]++ { print $5 " > " $2 > dupes; next } { print }' > "${fastdlfilelist}.sorted"
+	mv -f "${fastdlfilelist}.sorted" "${fastdlfilelist}"
+	if [ -f "${fastdlduplicatelist}" ]; then
+		duplicatefiles=$(wc -l < "${fastdlduplicatelist}")
+		while read -r duplicatefile; do
+			fn_script_log_info "Skipping ${duplicatefile}, already provided by a higher priority path"
+		done < "${fastdlduplicatelist}"
+	fi
+
+	declare -A fastdlrecordsource=()
+	declare -A fastdlexpected=()
+	if [ "${fastdlrebuild}" != "on" ] && [ -f "${fastdlrecord}" ] && [ "$(head -n 1 "${fastdlrecord}")" == "${fastdldir}" ]; then
+		while IFS=$'\t' read -r fastdltarget recordsource; do
+			[ -n "${fastdltarget}" ] && fastdlrecordsource["${fastdltarget}"]="${recordsource}"
+		done < <(tail -n +2 "${fastdlrecord}")
+	fi
+	echo "${fastdldir}" > "${fastdlnewrecord}"
+
+	read -ra fastdlexcludearray <<< "${fastdlexclude}"
+	while IFS=$'\t' read -r priority fastdlrelpath filesize filemtime fastdlfile; do
+		for excludepath in "${fastdlexcludearray[@]}"; do
+			# shellcheck disable=SC2254
+			case "${fastdlrelpath}" in
+				${excludepath#/}*)
+					((excludedfiles++))
+					continue 2
+					;;
+			esac
+		done
+		if [ "${engine}" == "goldsrc" ] || [ "${filesize}" -gt "${fastdlmaxcompresssize}" ]; then
+			fastdlmode="copy"
+			fastdltarget="${fastdlrelpath}"
+			if [ "${engine}" != "goldsrc" ]; then
+				((largefiles++))
+			fi
+		else
+			fastdlmode="compress"
+			fastdltarget="${fastdlrelpath}.bz2"
+		fi
+		fastdlsource="${fastdlfile}"$'\t'"${filesize}"$'\t'"${filemtime}"
+		fastdlexpected["${fastdltarget}"]="on"
+		printf '%s\t%s\n' "${fastdltarget}" "${fastdlsource}" >> "${fastdlnewrecord}"
+		echo "${fastdlrelpath}" >> "${fastdlclientlist}"
+		if [ ! -f "${fastdldir}/${fastdltarget}" ] || [ "${fastdlrecordsource["${fastdltarget}"]}" != "${fastdlsource}" ]; then
+			printf '%s\t%s\t%s\n' "${fastdlmode}" "${fastdlrelpath}" "${fastdlfile}" >> "${fastdlupdatelist}"
+			((updatefiles++))
+			filesizetotal=$((filesizetotal + filesize))
+			if [ "${fastdlmode}" == "compress" ]; then
+				((compressfiles++))
+			fi
+		else
+			((unchangedfiles++))
+		fi
+	done < "${fastdlfilelist}"
+
+	for fastdltarget in "${!fastdlrecordsource[@]}"; do
+		if [ -z "${fastdlexpected["${fastdltarget}"]}" ] && [ -f "${fastdldir}/${fastdltarget}" ]; then
+			echo "${fastdltarget}" >> "${fastdlremovelist}"
+			((removefiles++))
+		fi
+	done
+	if [ -d "${fastdldir}" ] && [ "${fastdlrebuild}" != "on" ]; then
+		while read -r fastdltarget; do
+			if [ -z "${fastdlexpected["${fastdltarget}"]}" ] && [ -z "${fastdlrecordsource["${fastdltarget}"]}" ]; then
+				((untrackedfiles++))
+			fi
+		done < <(find "${fastdldir}" -type f -printf '%P\n')
+	fi
+}
+
 # Provides info about the fastdl directory content and prompts for confirmation.
 fn_fastdl_preview() {
 	# Remove any file list.
-	if [ -f "${tmpdir}/fastdl_files_to_compress.txt" ]; then
-		rm -f "${tmpdir:?}/fastdl_files_to_compress.txt"
-	fi
+	rm -f "${fastdlfilelist:?}" "${fastdlduplicatelist:?}" "${fastdlnewrecord:?}" "${fastdlclientlist:?}" "${fastdlupdatelist:?}" "${fastdlremovelist:?}" "${fastdlcompresslist:?}"
+	touch "${fastdlclientlist}"
+	totalfiles=0
+	untrackedfiles=0
+	duplicatefiles=0
+	excludedfiles=0
+	largefiles=0
+	updatefiles=0
+	compressfiles=0
+	unchangedfiles=0
+	removefiles=0
+	filesizetotal=0
 	fn_print_nl "analysing required files"
 	fn_script_log_info "Analysing required files"
 	# Garry's Mod
 	if [ "${shortname}" == "gmod" ]; then
-		cd "${systemdir}" || exit
-		allowed_extentions_array=("*.ain" "*.bsp" "*.mdl" "*.mp3" "*.ogg" "*.otf" "*.pcf" "*.phy" "*.png" "*.svg" "*.vtf" "*.vmt" "*.vtx" "*.vvd" "*.ttf" "*.wav")
-		for allowed_extention in "${allowed_extentions_array[@]}"; do
-			fileswc=0
-			tput sc
-			while read -r ext; do
-				((fileswc++))
-				tput rc
-				tput el
-				fn_print "gathering ${allowed_extention} : ${fileswc}"
-				echo -e "${ext}" >> "${tmpdir}/fastdl_files_to_compress.txt"
-			done < <(find . -type f -iname "${allowed_extention}")
-			if [ ${fileswc} != 0 ]; then
-				fn_print_ok_eol_nl
-			else
-				fn_print_skip_eol_nl
-			fi
-		done
+		fn_fastdl_gather_gmod
 	# Source and GoldSrc engines
 	else
 		if [ "${engine}" == "goldsrc" ]; then
@@ -188,219 +349,124 @@ fn_fastdl_preview() {
 		else
 			fastdl_directories_array=("maps" "materials" "models" "particles" "sound" "resource")
 		fi
+		fastdl_custom_array=()
+		if [ "${engine}" == "source" ] && [ -d "${systemdir}/custom" ]; then
+			while IFS= read -r -d '' customdir; do
+				fastdl_custom_array+=("${customdir}")
+			done < <(find "${systemdir}/custom" -mindepth 1 -maxdepth 1 -type d -print0 | LC_ALL=C sort -z)
+		fi
 		for directory in "${fastdl_directories_array[@]}"; do
-			if [ -d "${systemdir}/${directory}" ]; then
-				if [ "${engine}" == "goldsrc" ]; then
-					fn_fastdl_goldsrc_extensions
-				elif [ "${directory}" == "maps" ]; then
-					local allowed_extentions_array=("*.bsp" "*.ain" "*.nav" "*.jpg" "*.txt")
-				elif [ "${directory}" == "materials" ]; then
-					local allowed_extentions_array=("*.vtf" "*.vmt" "*.vbf" "*.png" "*.svg")
-				elif [ "${directory}" == "models" ]; then
-					local allowed_extentions_array=("*.vtx" "*.vvd" "*.mdl" "*.phy" "*.jpg" "*.png" "*.vmt" "*.vtf")
-				elif [ "${directory}" == "particles" ]; then
-					local allowed_extentions_array=("*.pcf")
-				elif [ "${directory}" == "sound" ]; then
-					local allowed_extentions_array=("*.wav" "*.mp3" "*.ogg")
-				elif [ "${directory}" == "resource" ]; then
-					local allowed_extentions_array=("*")
+			if [ "${engine}" == "goldsrc" ]; then
+				fn_fastdl_goldsrc_extensions
+			else
+				fn_fastdl_source_extensions
+			fi
+			for customdir in "${fastdl_custom_array[@]}"; do
+				if [ -d "${customdir}/${directory}" ]; then
+					fn_fastdl_gather "${customdir}" "0:${customdir##*/}"
 				fi
-				for allowed_extention in "${allowed_extentions_array[@]}"; do
-					fileswc=0
-					tput sc
-					while read -r ext; do
-						((fileswc++))
-						tput rc
-						tput el
-						fn_print "gathering ${directory} ${allowed_extention} : ${fileswc}"
-						echo -e "${ext}" >> "${tmpdir}/fastdl_files_to_compress.txt"
-					done < <(find "${systemdir}/${directory}" "${fastdl_find_maxdepth[@]}" -type f -iname "${allowed_extention}")
-					tput rc
-					tput el
-					fn_print "gathering ${directory} ${allowed_extention} : ${fileswc}"
-					if [ ${fileswc} != 0 ]; then
-						fn_print_ok_eol_nl
-					else
-						fn_print_info_eol_nl
-					fi
-				done
+			done
+			if [ -d "${systemdir}/${directory}" ]; then
+				fn_fastdl_gather "${systemdir}" "1"
 			fi
 		done
 	fi
-	if [ -f "${tmpdir}/fastdl_files_to_compress.txt" ]; then
-		fn_print_nl "calculating total file size"
+	if [ -f "${fastdlfilelist}" ]; then
+		fn_print_nl "calculating changes"
 		fn_sleep_time_1
-		totalfiles=$(wc -l < "${tmpdir}/fastdl_files_to_compress.txt")
-		# Calculates total file size.
-		while read -r dufile; do
-			filesize=$(stat -c %s "${dufile}")
-			filesizetotal=$((filesizetotal + filesize))
-			exitcode=$?
-			if [ "${exitcode}" -ne 0 ]; then
-				fn_print_fail_eol_nl
-				fn_script_log_fail "Calculating total file size"
-				core_exit.sh
-			fi
-		done < "${tmpdir}/fastdl_files_to_compress.txt"
+		totalfiles=$(wc -l < "${fastdlfilelist}")
+		fn_fastdl_plan
 	else
 		fn_print_fail_eol_nl "generating file list"
 		fn_script_log_fail "Generating file list."
 		core_exit.sh
 	fi
 
-	if [ "${engine}" == "source" ]; then
-		echo -e "about to compress ${totalfiles} files, total size $(fn_human_readable_file_size "${filesizetotal}" 0)"
-	elif [ "${engine}" == "goldsrc" ]; then
-		echo -e "about to copy ${totalfiles} files, total size $(fn_human_readable_file_size "${filesizetotal}" 0)"
+	echo -e "found ${totalfiles} files: ${unchangedfiles} unchanged, ${excludedfiles} excluded, ${duplicatefiles} duplicate"
+	if [ "${updatefiles}" -gt 0 ]; then
+		if [ "${engine}" == "source" ]; then
+			echo -e "about to update ${updatefiles} files, ${compressfiles} to compress, total size $(fn_human_readable_file_size "${filesizetotal}" 0)"
+		elif [ "${engine}" == "goldsrc" ]; then
+			echo -e "about to copy ${updatefiles} files, total size $(fn_human_readable_file_size "${filesizetotal}" 0)"
+		fi
+	fi
+	if [ "${largefiles}" -gt 0 ]; then
+		echo -e "${largefiles} files over 150 MB will be copied without compression"
+	fi
+	if [ "${removefiles}" -gt 0 ]; then
+		echo -e "about to remove ${removefiles} files no longer on the server"
+	fi
+	if [ "${untrackedfiles}" -gt 0 ]; then
+		echo -e "${untrackedfiles} files in the Fastdl directory were not added by ${selfname} and will be left alone"
+	fi
+	if [ "${updatefiles}" -eq 0 ] && [ "${removefiles}" -eq 0 ]; then
+		echo -e "Fastdl directory is up to date"
 	fi
 
-	fn_script_log_info "${totalfiles} files, total size $(fn_human_readable_file_size "${filesizetotal}" 0)"
-	rm -f "${tmpdir:?}/fastdl_files_to_compress.txt"
+	fn_script_log_info "${totalfiles} files, ${updatefiles} to update, ${unchangedfiles} unchanged, ${excludedfiles} excluded, ${removefiles} to remove, total size $(fn_human_readable_file_size "${filesizetotal}" 0)"
 	if ! fn_prompt_yn "Continue?" Y; then
 		exitcode=0
 		core_exit.sh
 	fi
 }
 
-# Builds Garry's Mod fastdl directory content.
-fn_fastdl_gmod() {
-	cd "${systemdir}" || exit
-	for allowed_extention in "${allowed_extentions_array[@]}"; do
+# Removes files from the fastdl directory that are no longer on the server.
+fn_fastdl_remove() {
+	if [ -s "${fastdlremovelist}" ]; then
+		fn_print "removing ${removefiles} files no longer on the server"
+		while read -r fastdlstalefile; do
+			rm -f "${fastdldir:?}/${fastdlstalefile}"
+			exitcode=$?
+			if [ "${exitcode}" -ne 0 ]; then
+				fn_print_fail_eol_nl
+				fn_script_log_fail "Removing ${fastdldir}/${fastdlstalefile}"
+				core_exit.sh
+			else
+				fn_script_log_pass "Removing ${fastdldir}/${fastdlstalefile}"
+			fi
+		done < "${fastdlremovelist}"
+		find "${fastdldir:?}" -mindepth 1 -type d -empty -delete
+		fn_print_ok_eol_nl
+	fi
+}
+
+# Copies new and changed files to the fastdl directory.
+fn_fastdl_copy() {
+	if [ -s "${fastdlupdatelist}" ]; then
 		fileswc=0
 		tput sc
-		while read -r fastdlfile; do
+		while IFS=$'\t' read -r fastdlmode fastdlrelpath fastdlfile; do
 			((fileswc++))
 			tput rc
 			tput el
-			fn_print "copying ${allowed_extention} : ${fileswc}"
-			cp --parents "${fastdlfile}" "${fastdldir}"
+			fn_print "copying files : ${fileswc}/${updatefiles}"
+			fastdltarget="${fastdldir}/${fastdlrelpath}"
+			mkdir -p "${fastdltarget%/*}" && cp -p "${fastdlfile}" "${fastdltarget}"
 			exitcode=$?
 			if [ "${exitcode}" -ne 0 ]; then
 				fn_print_fail_eol_nl
-				fn_script_log_fail "Copying ${fastdlfile} > ${fastdldir}"
+				fn_script_log_fail "Copying ${fastdlfile} > ${fastdltarget}"
 				core_exit.sh
 			else
-				fn_script_log_pass "Copying ${fastdlfile} > ${fastdldir}"
+				fn_script_log_pass "Copying ${fastdlfile} > ${fastdltarget}"
 			fi
-		done < <(find . -type f -iname "${allowed_extention}")
-		if [ ${fileswc} != 0 ]; then
-			fn_print_ok_eol_nl
-		fi
-	done
-	# Correct addons directory structure for Fastdl.
-	if [ -d "${fastdldir}/addons" ]; then
-		fn_print "updating addons file structure..."
-		cp -Rf "${fastdldir}"/addons/*/* "${fastdldir}"
-		exitcode=$?
-		if [ "${exitcode}" -ne 0 ]; then
-			fn_print_fail_eol_nl
-			fn_script_log_fail "Updating addons file structure"
-			core_exit.sh
-		else
-			fn_print_ok_eol_nl
-			fn_script_log_pass "Updating addons file structure"
-		fi
-		# Clear addons directory in fastdl.
-		fn_print "clearing addons dir from fastdl dir..."
-		fn_sleep_time_1
-		rm -rf "${fastdldir:?}/addons"
-		exitcode=$?
-		if [ "${exitcode}" -ne 0 ]; then
-			fn_print_fail_eol_nl
-			fn_script_log_fail "Clearing addons dir from fastdl dir"
-			core_exit.sh
-		else
-			fn_print_ok_eol_nl
-			fn_script_log_pass "Clearing addons dir from fastdl dir"
-		fi
-	fi
-	# Correct content that may be into a lua directory by mistake like some darkrpmodification addons.
-	if [ -d "${fastdldir}/lua" ]; then
-		fn_print "correcting DarkRP files..."
-		fn_sleep_time_1
-		cp -Rf "${fastdldir}/lua/"* "${fastdldir}"
-		exitcode=$?
-		if [ "${exitcode}" -ne 0 ]; then
-			fn_print_fail_eol_nl
-			fn_script_log_fail "Correcting DarkRP files"
-			core_exit.sh
-		else
-			fn_print_ok_eol_nl
-			fn_script_log_pass "Correcting DarkRP files"
-		fi
-	fi
-	if [ -f "${tmpdir}/fastdl_files_to_compress.txt" ]; then
-		totalfiles=$(wc -l < "${tmpdir}/fastdl_files_to_compress.txt")
-		# Calculates total file size.
-		while read -r dufile; do
-			filesize=$(du -b "${dufile}" | awk '{ print $1 }')
-			filesizetotal=$((filesizetotal + filesize))
-		done < "${tmpdir}/fastdl_files_to_compress.txt"
-	fi
-}
-
-fn_fastdl_source() {
-	for directory in "${fastdl_directories_array[@]}"; do
-		if [ -d "${systemdir}/${directory}" ]; then
-			if [ "${engine}" == "goldsrc" ]; then
-				fn_fastdl_goldsrc_extensions
-			elif [ "${directory}" == "maps" ]; then
-				local allowed_extentions_array=("*.bsp" "*.ain" "*.nav" "*.jpg" "*.txt")
-			elif [ "${directory}" == "materials" ]; then
-				local allowed_extentions_array=("*.vtf" "*.vmt" "*.vbf" "*.png" "*.svg")
-			elif [ "${directory}" == "models" ]; then
-				local allowed_extentions_array=("*.vtx" "*.vvd" "*.mdl" "*.phy" "*.jpg" "*.png")
-			elif [ "${directory}" == "particles" ]; then
-				local allowed_extentions_array=("*.pcf")
-			elif [ "${directory}" == "sound" ]; then
-				local allowed_extentions_array=("*.wav" "*.mp3" "*.ogg")
-			elif [ "${directory}" == "resource" ]; then
-				local allowed_extentions_array=("*")
+			if [ "${fastdlmode}" == "compress" ]; then
+				printf '%s\0' "${fastdltarget}" >> "${fastdlcompresslist}"
 			fi
-			for allowed_extention in "${allowed_extentions_array[@]}"; do
-				fileswc=0
-				tput sc
-				while read -r fastdlfile; do
-					((fileswc++))
-					tput rc
-					tput el
-					fn_print "copying ${directory} ${allowed_extention} : ${fileswc}"
-					fn_sleep_time
-					# get relative path of file in the dir
-					tmprelfilepath="${fastdlfile#"${systemdir}/"}"
-					copytodir="${tmprelfilepath%/*}"
-					# create relative path for fastdl
-					if [ ! -d "${fastdldir}/${copytodir}" ]; then
-						mkdir -p "${fastdldir}/${copytodir}"
-					fi
-					cp "${fastdlfile}" "${fastdldir}/${copytodir}"
-					exitcode=$?
-					if [ "${exitcode}" -ne 0 ]; then
-						fn_print_fail_eol_nl
-						fn_script_log_fail "Copying ${fastdlfile} > ${fastdldir}/${copytodir}"
-						core_exit.sh
-					else
-						fn_script_log_pass "Copying ${fastdlfile} > ${fastdldir}/${copytodir}"
-					fi
-				done < <(find "${systemdir}/${directory}" "${fastdl_find_maxdepth[@]}" -type f -iname "${allowed_extention}")
-				if [ ${fileswc} != 0 ]; then
-					fn_print_ok_eol_nl
-				fi
-			done
-		fi
-	done
+		done < "${fastdlupdatelist}"
+		fn_print_ok_eol_nl
+	fi
 }
 
 # Builds the fastdl directory content.
 fn_fastdl_build() {
 	# Copy all needed files for Fastdl.
-	fn_print_nl "copying files to ${fastdldir}"
-	fn_script_log_info "Copying files to ${fastdldir}"
+	fn_print_nl "updating files in ${fastdldir}"
+	fn_script_log_info "Updating files in ${fastdldir}"
+	fn_fastdl_remove
+	fn_fastdl_copy
 	if [ "${shortname}" == "gmod" ]; then
-		fn_fastdl_gmod
 		fn_fastdl_gmod_dl_enforcer
-	else
-		fn_fastdl_source
 	fi
 }
 
@@ -427,7 +493,7 @@ fn_fastdl_gmod_dl_enforcer() {
 		# Read all filenames and put them into a lua file at the right path.
 		while read -r line; do
 			echo -e "resource.AddFile( \"${line}\" )" >> "${luafastdlfullpath}"
-		done < <(find "${fastdldir:?}" \( -type f ! -name "*.bz2" \) -printf '%P\n')
+		done < "${fastdlclientlist}"
 		exitcode=$?
 		if [ "${exitcode}" -ne 0 ]; then
 			fn_print_fail_eol_nl
@@ -442,30 +508,61 @@ fn_fastdl_gmod_dl_enforcer() {
 
 # Compresses Fastdl files using bzip2.
 fn_fastdl_bzip2() {
-	while read -r filetocompress; do
-		fn_print "compressing ${filetocompress}"
-		bzip2 -f "${filetocompress}"
-		exitcode=$?
-		if [ "${exitcode}" -ne 0 ]; then
+	if [ -s "${fastdlcompresslist}" ]; then
+		fastdlthreads="$(nproc)"
+		fn_print "compressing ${compressfiles} files using ${fastdlthreads} threads"
+		xargs -0 -r -n 1 -P "${fastdlthreads}" bzip2 -f < "${fastdlcompresslist}"
+		compressexitcode=$?
+		while IFS=$'\t' read -r fastdlmode fastdlrelpath fastdlfile; do
+			if [ "${fastdlmode}" == "compress" ]; then
+				filetocompress="${fastdldir}/${fastdlrelpath}"
+				if [ -f "${filetocompress}.bz2" ] && [ ! -f "${filetocompress}" ]; then
+					fn_script_log_pass "Compressing ${filetocompress}"
+				else
+					fn_script_log_fail "Compressing ${filetocompress}"
+				fi
+			fi
+		done < "${fastdlupdatelist}"
+		if [ "${compressexitcode}" -ne 0 ]; then
 			fn_print_fail_eol_nl
-			fn_script_log_fail "Compressing ${filetocompress}"
+			fn_script_log_fail "Compressing files"
+			exitcode=1
 			core_exit.sh
 		else
 			fn_print_ok_eol_nl
-			fn_script_log_pass "Compressing ${filetocompress}"
+			fn_script_log_pass "Compressing files"
 		fi
-	done < <(find "${fastdldir:?}" \( -type f ! -name "*.bz2" \))
+	fi
+}
+
+# Saves the list of files written to the fastdl directory.
+fn_fastdl_record() {
+	fn_print "saving Fastdl file record ${fastdlrecord}"
+	mkdir -p "${datadir}" && mv -f "${fastdlnewrecord}" "${fastdlrecord}"
+	exitcode=$?
+	if [ "${exitcode}" -ne 0 ]; then
+		fn_print_fail_eol_nl
+		fn_script_log_fail "Saving Fastdl file record ${fastdlrecord}"
+		core_exit.sh
+	else
+		fn_print_ok_eol_nl
+		fn_script_log_pass "Saving Fastdl file record ${fastdlrecord}"
+	fi
 }
 
 check.sh
 # Run functions.
 fn_fastdl_preview
-fn_clear_old_fastdl
+if [ "${fastdlrebuild}" == "on" ]; then
+	fn_clear_old_fastdl
+fi
 fn_fastdl_dirs
 fn_fastdl_build
 if [ "${engine}" == "source" ]; then
 	fn_fastdl_bzip2
 fi
+fn_fastdl_record
+rm -f "${fastdlfilelist:?}" "${fastdlduplicatelist:?}" "${fastdlnewrecord:?}" "${fastdlclientlist:?}" "${fastdlupdatelist:?}" "${fastdlremovelist:?}" "${fastdlcompresslist:?}"
 # Finished message.
 fn_print_nl "Fastdl files are located in:"
 fn_print_nl "${fastdldir}"