Skip to content

Instantly share code, notes, and snippets.

@Jwink3101
Created December 25, 2025 16:49
Show Gist options
  • Select an option

  • Save Jwink3101/9845ff48917e71a637740cfd35443cd7 to your computer and use it in GitHub Desktop.

Select an option

Save Jwink3101/9845ff48917e71a637740cfd35443cd7 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
"""
This is my generic script to mount[1] an rclone[2] crypt[3] locally[4], with some
tuning for that specific use case.
It is designed for macOS and FUSE-T[5] but should work on all.
This assumes 'config.cfg' is in the same directory of the script.
NOTE: The config is encrypted with rclone's native config encryption[6]
and it will let rclone prompt for the password.
[1]: https://rclone.org/commands/rclone_mount/
[2]: https://rclone.org/
[3]: https://rclone.org/crypt/
[4]: https://rclone.org/local/
[5]: https://www.fuse-t.org/
[6]: https://rclone.org/docs/#configuration-encryption
Built in conjunction with ChatGPT 5.2 but *not* "vibe coded"
"""
import os
import sys
import subprocess
import tempfile
import shutil
import signal
import atexit
from pathlib import Path
################################ Make sure in GNU Screen ################################
if 'STY' not in os.environ:
print("Not running in a GNU Screen session. Relaunching in Screen...")
# Relaunch the script in a new Screen session with all arguments
name = f"enc.{os.getpid()}" # Make sure it is unique (enough)
subprocess.run(['screen', '-S', name, sys.executable] + sys.argv)
sys.exit()
#########################################################################################
# Make sure you are executing in the current directory of the file
os.chdir(os.path.dirname(os.path.abspath(__file__)))
MNT= Path("~/Desktop/_priv").expanduser()
MNT.mkdir(parents=True,exist_ok=False) # Will error if it exists
# This uses the OS's tempdir. But if the script is killed hard (e.g. -9), it will
# not get cleaned. You could instead use the Desktop so you can confirm it's cleared
# or use any other path.
TMP = Path(tempfile.mkdtemp(prefix='PRIV.',suffix='.PRIV'))
#TMP = Path("~/Desktop/__TEMP_DELETE_ME__").expanduser()
temp = TMP / 'temp'
cache = TMP / 'cache'
temp.mkdir(parents=True,exist_ok=False)
cache.mkdir(parents=True,exist_ok=False)
env = os.environ.copy()
env['RCLONE_TEMP_DIR'] = str(temp)
env['RCLONE_CACHE_DIR'] = str(cache)
env['RCLONE_CONFIG'] = 'config.cfg' # Update for your config path
env.pop('RCLONE_PASSWORD_COMMAND',0)
# Track rclone explicitly so we can terminate it during cleanup
rclone_proc = None
cleaned = False
def cleanup(signum=None, frame=None):
"""
Best-effort cleanup.
Safe to call multiple times.
"""
global cleaned, rclone_proc
if cleaned:
return
cleaned = True
print("\nCleaning up…")
# Terminate rclone if it is still running
if rclone_proc and rclone_proc.poll() is None:
try:
rclone_proc.terminate()
rclone_proc.wait(timeout=5)
except Exception:
pass
# This is macOS specific. Need to update for Linux
subprocess.run(["umount", "-f", str(MNT)], check=False, env=env)
shutil.rmtree(TMP, ignore_errors=True)
print(f"Fully removed (rm -rf) {str(TMP)!r}")
try:
os.rmdir(MNT)
print(f"Removed Mount (rmdir) {str(MNT)!r}")
except OSError:
pass
if signum is not None:
sys.exit(128 + signum)
# Explicit signal handling (covers far more cases than KeyboardInterrupt alone)
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT):
signal.signal(sig, cleanup)
# Fallback only — not relied upon
atexit.register(cleanup)
command = [
'rclone',
# Mounting method
#'nfsmount',
'mount',
#'-o','backend=smb', # https://forum.rclone.org/t/macos-rclone-mount-new-fuse-t-released-old-issues-fixed/39502/13?u=jwink3101
'crypt:', MNT,
# Options
'-v',
'--volname', 'PRIV',
'--vfs-links', # translate to/from .rclonelinks
## Cache Settings tuned for local
# Reading from local is FAST so just cache writes. If this were a non-local remote
# I would use 'full'
'--vfs-cache-mode', 'writes',
'--vfs-cache-max-age', '15m',
'--vfs-cache-max-size', '2G',
'--vfs-fast-fingerprint',
'--vfs-write-back', '5s',
'--vfs-cache-poll-interval', '20s',
'--vfs-case-insensitive',
# '--file-perms', '0777', # Uncomment if needed
# Stats printing
'--stats-one-line', '--stats', '20s',
# Apple cruft
'--noappledouble', '--noapplexattr',
'--filter', '- ._*', # Make sure resource forks aren't written
# This ignores symlinks in the encrypted dir. Uncomment if you want them followed
'--skip-links',
]
try:
rclone_proc = subprocess.Popen(
[str(c) for c in command], # make sure all strings, not ints or Paths
env=env,
start_new_session=True, # This makes sure that CTRL-C is not sent to rclone.
# Cleanup handles termination.
)
rclone_proc.wait()
finally:
cleanup()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment