Class: Clacky::Server::BackupManager

Inherits:
Object
  • Object
show all
Defined in:
lib/clacky/server/backup_manager.rb

Overview

Backs up the user’s ~/.clacky directory to a safe location.

Design notes:

* Regenerable caches/logs are always excluded to keep archives small.
* On WSL, the default destination is a Windows drive (/mnt/c|d|e) so
  backups survive a WSL distro reset.
* Session history (sessions/ + snapshots/) is optional — it is the
  bulk of the data and the user may not want it in every archive.
* Config lives in ~/.clacky/backup.yml, separate from config.yml so
  it never mixes with API keys.

Constant Summary collapse

CLACKY_DIR =
File.expand_path("~/.clacky")
CONFIG_FILE =
File.join(CLACKY_DIR, "backup.yml")
ALWAYS_EXCLUDE =

Always excluded — regenerable or disposable.

%w[
  ocr_cache parsers parsers-1 logger safety_logs trash .DS_Store backup.yml
].freeze
HEAVY_EXCLUDE =

Excluded unless the user opts into a full backup.

%w[sessions snapshots].freeze
DEFAULT_CONFIG =
{
  "enabled"          => false,
  "cron"             => "0 3 * * *",
  "dest_dir"         => nil,
  "keep"             => 7,
  "include_sessions" => true,
  "last_run_at"      => nil,
  "last_status"      => nil,
  "last_error"       => nil,
  "last_archive"     => nil
}.freeze

Class Method Summary collapse

Class Method Details

.build_download!Object

Build a one-off archive for direct download (not written to dest_dir, not pruned, not recorded). Always includes session history so the downloaded file is a complete snapshot. Caller is responsible for deleting the returned temp file after streaming it.



91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/clacky/server/backup_manager.rb', line 91

def build_download!
  stamp    = Time.now.strftime("%Y%m%d-%H%M%S")
  filename = "clacky-backup-#{stamp}.tar.gz"
  archive  = File.join(Dir.tmpdir, filename)

  ok = build_archive(archive, ALWAYS_EXCLUDE.dup)
  unless ok && File.exist?(archive)
    FileUtils.rm_f(archive)
    raise "Backup failed: tar did not produce an archive"
  end

  { path: archive, filename: filename, size: File.size(archive) }
end

.configObject



45
46
47
# File 'lib/clacky/server/backup_manager.rb', line 45

def config
  DEFAULT_CONFIG.merge(load_raw)
end

.default_destObject

Where archives go when the user hasn’t set an explicit dest_dir.



143
144
145
146
147
148
149
150
151
# File 'lib/clacky/server/backup_manager.rb', line 143

def default_dest
  if wsl?
    %w[d c e].each do |drive|
      mount = "/mnt/#{drive}"
      return File.join(mount, "clacky_backups") if Dir.exist?(mount) && File.writable?(mount)
    end
  end
  File.expand_path("~/clacky_backups")
end

.listObject

List existing backup archives at the resolved destination.



106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/clacky/server/backup_manager.rb', line 106

def list
  dest = resolve_dest(config["dest_dir"])
  return [] unless Dir.exist?(dest)

  Dir.glob(File.join(dest, "clacky-backup-*.tar.gz")).map do |path|
    {
      "name"       => File.basename(path),
      "path"       => path,
      "size"       => File.size(path),
      "created_at" => File.mtime(path).iso8601
    }
  end.sort_by { |b| b["created_at"] }.reverse
end

.run!Object

Run a backup now. Returns a hash describing the result.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/clacky/server/backup_manager.rb', line 61

def run!
  cfg     = config
  dest    = resolve_dest(cfg["dest_dir"])
  FileUtils.mkdir_p(dest)

  stamp   = Time.now.strftime("%Y%m%d-%H%M%S")
  archive = File.join(dest, "clacky-backup-#{stamp}.tar.gz")
  excludes = ALWAYS_EXCLUDE.dup
  excludes.concat(HEAVY_EXCLUDE) unless cfg["include_sessions"]

  ok = build_archive(archive, excludes)
  unless ok && File.exist?(archive)
    record_result(cfg, status: "error", error: "tar failed", archive: nil)
    raise "Backup failed: tar did not produce an archive"
  end

  prune(dest, cfg["keep"])
  record_result(cfg, status: "success", error: nil, archive: archive)

  { archive: archive, size: File.size(archive), dest_dir: dest }
rescue => e
  Clacky::Logger.error("backup_run_error", error: e) if defined?(Clacky::Logger)
  record_result(config, status: "error", error: e.message, archive: nil)
  raise
end

.statusObject

Resolved destination + whether we’re on WSL (for UI display).



121
122
123
124
125
126
127
128
129
# File 'lib/clacky/server/backup_manager.rb', line 121

def status
  dest = resolve_dest(config["dest_dir"])
  {
    "config"   => config,
    "dest_dir" => dest,
    "is_wsl"   => wsl?,
    "backups"  => list
  }
end

.update_config(enabled: nil, cron: nil, dest_dir: nil, keep: nil, include_sessions: nil) ⇒ Object



49
50
51
52
53
54
55
56
57
58
# File 'lib/clacky/server/backup_manager.rb', line 49

def update_config(enabled: nil, cron: nil, dest_dir: nil, keep: nil, include_sessions: nil)
  cfg = config
  cfg["enabled"]          = !!enabled            unless enabled.nil?
  cfg["cron"]             = cron.to_s            unless cron.nil?
  cfg["dest_dir"]         = normalize_dest(dest_dir) unless dest_dir.nil?
  cfg["keep"]             = [keep.to_i, 1].max   unless keep.nil?
  cfg["include_sessions"] = !!include_sessions   unless include_sessions.nil?
  save_raw(cfg)
  cfg
end

.wsl?Boolean

Returns:

  • (Boolean)


131
132
133
134
135
136
137
138
# File 'lib/clacky/server/backup_manager.rb', line 131

def wsl?
  @wsl ||= begin
    File.exist?("/proc/version") &&
      File.read("/proc/version").match?(/microsoft|wsl/i)
  rescue StandardError
    false
  end
end