update.lua
289 lines · 8.7 KB
Bulk updater for CC-Tweaked scripts
Usage: run: update
Copy & run
wget https://perlytiara.github.io/turtles.tips/raw/programs/perlytiara/BigBaemingGamers/update.lua
| 1 | -- Bulk updater for CC-Tweaked scripts |
| 2 | -- Overwrites local scripts with the latest from GitHub |
| 3 | -- Usage: run: update |
| 4 | |
| 5 | local BASE = 'https://raw.githubusercontent.com/perlytiara/CC-Tweaked-TurtsAndComputers/refs/heads/main/' |
| 6 | |
| 7 | local manifest = { |
| 8 | ["programs/perlytiara/BigBaemingGamers"] = { |
| 9 | -- client |
| 10 | "client/client-bomb.lua", |
| 11 | "client/client-dig.lua", |
| 12 | "client/client-quarry.lua", |
| 13 | "client/client.lua", |
| 14 | -- farm |
| 15 | "farm/cactus-farm.lua", |
| 16 | "farm/harvest_0.lua", |
| 17 | "farm/harvest_1.lua", |
| 18 | -- phone |
| 19 | "phone/phone-app-mine.lua", |
| 20 | "phone/phone-bombing.lua", |
| 21 | -- server |
| 22 | "server/server-phone.lua", |
| 23 | -- tasks |
| 24 | "tasks/quarry-miner.lua", |
| 25 | "tasks/simple-quarry.lua", |
| 26 | -- tnt |
| 27 | "tnt/tnt-deployer.lua", |
| 28 | "tnt/tnt-igniter.lua", |
| 29 | -- update helpers |
| 30 | "update/update_bamboo.lua", |
| 31 | "update/update_dispense.lua", |
| 32 | "update/update_lift.lua", |
| 33 | "update/update_pos.lua", |
| 34 | -- utils |
| 35 | "utils/block-placer.lua", |
| 36 | "utils/detectids.lua", |
| 37 | "utils/item-counter.lua", |
| 38 | "utils/lava-refueler.lua", |
| 39 | "utils/ore-keeper.lua", |
| 40 | "utils/ractor.lua", |
| 41 | "utils/refuel-test.lua", |
| 42 | "utils/upward-quarry-test.lua", |
| 43 | -- loader and updater aliases |
| 44 | "@load.lua", |
| 45 | "@update.lua", |
| 46 | "load.lua", |
| 47 | "update.lua", |
| 48 | }, |
| 49 | ["programs/perlytiara/BigBaemingGamers/@gps"] = { |
| 50 | -- @gps scripts |
| 51 | "@gps/gps.lua", |
| 52 | "@gps/gps-host.lua", |
| 53 | }, |
| 54 | } |
| 55 | |
| 56 | local function ensureDir(path) |
| 57 | if not fs.exists(path) then |
| 58 | fs.makeDir(path) |
| 59 | end |
| 60 | end |
| 61 | |
| 62 | local function ensureParentDir(localPath) |
| 63 | local parent = string.match(localPath, "(.+)/[^/]+$") |
| 64 | if parent then ensureDir(parent) end |
| 65 | end |
| 66 | |
| 67 | local function fetch(remotePath) |
| 68 | local url = BASE .. remotePath .. '?breaker=' .. tostring(math.random(0, 999999)) |
| 69 | local res, err = http.get(url) |
| 70 | if not res then |
| 71 | print('ERROR: http.get failed for ' .. remotePath .. ' - ' .. tostring(err)) |
| 72 | return false, nil |
| 73 | end |
| 74 | local code = res.readAll() |
| 75 | res.close() |
| 76 | return true, code |
| 77 | end |
| 78 | |
| 79 | local function writeFile(path, contents) |
| 80 | local handle = fs.open(path, 'w') |
| 81 | handle.write(contents) |
| 82 | handle.close() |
| 83 | end |
| 84 | |
| 85 | local function readFile(path) |
| 86 | if not fs.exists(path) then return '' end |
| 87 | local h = fs.open(path, 'r') |
| 88 | local c = h.readAll() |
| 89 | h.close() |
| 90 | return c or '' |
| 91 | end |
| 92 | |
| 93 | local function bytesDiff(a, b) |
| 94 | return string.len(b) - string.len(a) |
| 95 | end |
| 96 | |
| 97 | local function progressBar(current, total, label) |
| 98 | local width = 28 |
| 99 | local filled = math.floor((current / total) * width) |
| 100 | local bar = string.rep('#', filled) .. string.rep('-', width - filled) |
| 101 | print(string.format('[%s] %d/%d %s', bar, current, total, label or '')) |
| 102 | end |
| 103 | |
| 104 | local function removeStale(localDir, keepSet) |
| 105 | if not fs.exists(localDir) then return end |
| 106 | local function walk(dir, relPrefix) |
| 107 | local items = fs.list(dir) |
| 108 | for i = 1, #items, 1 do |
| 109 | local name = items[i] |
| 110 | local full = dir .. '/' .. name |
| 111 | local relPath = (relPrefix ~= '' and (relPrefix .. '/' .. name)) or name |
| 112 | if fs.isDir(full) then |
| 113 | walk(full, relPath) |
| 114 | -- remove empty dir |
| 115 | if #fs.list(full) == 0 then fs.delete(full) end |
| 116 | else |
| 117 | -- Do not delete the loader/updater themselves at root |
| 118 | if relPath ~= 'load.lua' and relPath ~= 'update.lua' and relPath ~= '@load.lua' and relPath ~= '@update.lua' and not keepSet[relPath] then |
| 119 | fs.delete(full) |
| 120 | print('Removed stale: ' .. full) |
| 121 | end |
| 122 | end |
| 123 | end |
| 124 | end |
| 125 | walk(localDir, '') |
| 126 | end |
| 127 | |
| 128 | local function updateDir(dir, files) |
| 129 | local updated, same = 0, 0 |
| 130 | local localDir = '/' .. dir |
| 131 | ensureDir(localDir) |
| 132 | |
| 133 | local keep = {} |
| 134 | for i = 1, #files, 1 do keep[files[i]] = true end |
| 135 | |
| 136 | local total = #files |
| 137 | for i = 1, #files, 1 do |
| 138 | local fname = files[i] |
| 139 | local remotePath = dir .. '/' .. fname |
| 140 | local localPath = localDir .. '/' .. fname |
| 141 | local ok, code = fetch(remotePath) |
| 142 | if ok and code then |
| 143 | local old = readFile(localPath) |
| 144 | if old == code then |
| 145 | same = same + 1 |
| 146 | else |
| 147 | ensureParentDir(localPath) |
| 148 | writeFile(localPath, code) |
| 149 | local diff = bytesDiff(old, code) |
| 150 | local change = (diff >= 0 and (tostring(math.abs(diff)) .. ' bytes added')) or (tostring(math.abs(diff)) .. ' bytes removed') |
| 151 | print('Updated: ' .. localPath .. ' (' .. change .. ')') |
| 152 | updated = updated + 1 |
| 153 | end |
| 154 | end |
| 155 | progressBar(i, total, fname) |
| 156 | sleep(0) |
| 157 | end |
| 158 | |
| 159 | removeStale(localDir, keep) |
| 160 | print(string.format('Done. Updated %d, unchanged %d.', updated, same)) |
| 161 | end |
| 162 | |
| 163 | local function promptSelect(options) |
| 164 | for i = 1, #options, 1 do |
| 165 | print(string.format('%d) %s', i, options[i])) |
| 166 | end |
| 167 | io.write('Select number: ') |
| 168 | local choice = read() |
| 169 | local idx = tonumber(choice) |
| 170 | if idx and idx >= 1 and idx <= #options then |
| 171 | return idx |
| 172 | end |
| 173 | return nil |
| 174 | end |
| 175 | |
| 176 | local function showMenu() |
| 177 | print('== Update Menu ==') |
| 178 | print('1) Auto update all (clean stale)') |
| 179 | print('2) Update a single file') |
| 180 | print('3) Remove a local file') |
| 181 | print('4) Exit') |
| 182 | io.write('Choose: ') |
| 183 | local ch = read() |
| 184 | return tonumber(ch) |
| 185 | end |
| 186 | |
| 187 | local function updateAll() |
| 188 | for dir, files in pairs(manifest) do |
| 189 | updateDir(dir, files) |
| 190 | end |
| 191 | end |
| 192 | |
| 193 | local function updateSingle() |
| 194 | local dirs = {} |
| 195 | for d, _ in pairs(manifest) do table.insert(dirs, d) end |
| 196 | print('Select directory:') |
| 197 | local dirIdx = promptSelect(dirs) |
| 198 | if not dirIdx then print('Invalid selection') return end |
| 199 | local dir = dirs[dirIdx] |
| 200 | local files = manifest[dir] |
| 201 | print('Select file to update:') |
| 202 | local idx = promptSelect(files) |
| 203 | if not idx then print('Invalid selection') return end |
| 204 | updateDir(dir, { files[idx] }) |
| 205 | end |
| 206 | |
| 207 | local function removeOne() |
| 208 | local dirs = {} |
| 209 | for d, _ in pairs(manifest) do table.insert(dirs, d) end |
| 210 | print('Select directory:') |
| 211 | local dirIdx = promptSelect(dirs) |
| 212 | if not dirIdx then print('Invalid selection') return end |
| 213 | local dir = dirs[dirIdx] |
| 214 | local files = manifest[dir] |
| 215 | print('Select file to remove locally:') |
| 216 | local idx = promptSelect(files) |
| 217 | if not idx then print('Invalid selection') return end |
| 218 | local localDir = '/' .. dir |
| 219 | local localPath = localDir .. '/' .. files[idx] |
| 220 | if fs.exists(localPath) then |
| 221 | fs.delete(localPath) |
| 222 | print('Removed: ' .. localPath) |
| 223 | else |
| 224 | print('Not found: ' .. localPath) |
| 225 | end |
| 226 | end |
| 227 | |
| 228 | if not http then |
| 229 | print('ERROR: http API not available. Enable in mod config.') |
| 230 | else |
| 231 | if #arg >= 1 then |
| 232 | -- CLI usage: |
| 233 | -- update all | update <filename> | rm <filename> |
| 234 | local cmd = tostring(arg[1]) |
| 235 | if cmd == 'all' then |
| 236 | updateAll() |
| 237 | elseif cmd == 'rm' and arg[2] then |
| 238 | local target = tostring(arg[2]) |
| 239 | local found = false |
| 240 | local foundDir = nil |
| 241 | for dir, files in pairs(manifest) do |
| 242 | for i = 1, #files, 1 do |
| 243 | if files[i] == target then |
| 244 | found = true |
| 245 | foundDir = dir |
| 246 | break |
| 247 | end |
| 248 | end |
| 249 | if found then break end |
| 250 | end |
| 251 | if found and foundDir then |
| 252 | local localDir = '/' .. foundDir |
| 253 | local localPath = localDir .. '/' .. target |
| 254 | if fs.exists(localPath) then fs.delete(localPath) print('Removed: ' .. localPath) else print('Not found: ' .. localPath) end |
| 255 | else |
| 256 | print('Unknown file: ' .. target) |
| 257 | end |
| 258 | else |
| 259 | -- assume single-file update by name |
| 260 | local target = cmd |
| 261 | local found = false |
| 262 | local foundDir = nil |
| 263 | for dir, files in pairs(manifest) do |
| 264 | for i = 1, #files, 1 do |
| 265 | if files[i] == target then |
| 266 | found = true |
| 267 | foundDir = dir |
| 268 | break |
| 269 | end |
| 270 | end |
| 271 | if found then break end |
| 272 | end |
| 273 | if found and foundDir then updateDir(foundDir, { target }) else print('Unknown file: ' .. target) end |
| 274 | end |
| 275 | else |
| 276 | while true do |
| 277 | local ch = showMenu() |
| 278 | if ch == 1 then |
| 279 | updateAll() |
| 280 | elseif ch == 2 then |
| 281 | updateSingle() |
| 282 | elseif ch == 3 then |
| 283 | removeOne() |
| 284 | else |
| 285 | break |
| 286 | end |
| 287 | end |
| 288 | end |
| 289 | end |