-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinit.lua
More file actions
261 lines (208 loc) · 8.1 KB
/
Copy pathinit.lua
File metadata and controls
261 lines (208 loc) · 8.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
-- ============================================================
-- luanti_git_backups | init.lua
-- Automatically snapshots your Minetest world via Git.
--
-- SETUP (minetest.conf):
-- secure.trusted_mods = luanti_git_backups
-- luanti_git_backup_interval = 900 (seconds, default 15 min)
-- ============================================================
-- ============================================================
-- SECTION 1: SECURITY
-- ============================================================
local ie = minetest.request_insecure_environment()
if not ie then
minetest.log("error", "[luanti_git_backups] Mod not trusted! Add it to secure.trusted_mods in minetest.conf.")
return
end
-- ============================================================
-- SECTION 2: NAMESPACE
-- ============================================================
luanti_git_backups = {}
local M = luanti_git_backups
-- ============================================================
-- SECTION 3: CONFIGURATION
-- ============================================================
M.MOD_TAG = "[luanti_git_backups]"
M.world_path = minetest.get_worldpath()
M.revert_countdown = 5
local backup_timer = 0
local backup_interval = tonumber(minetest.settings:get("luanti_git_backup_interval")) or 900
-- ============================================================
-- SECTION 4: UTILITIES
-- ============================================================
function M.shell_exec(cmd)
local f = ie.io.popen(cmd)
if not f then return "" end
local s = f:read("*a")
f:close()
return s:gsub("^%s*(.-)%s*$", "%1")
end
local function log(level, msg)
minetest.log(level, M.MOD_TAG .. " " .. msg)
end
local function broadcast(msg)
minetest.chat_send_all(M.MOD_TAG .. " " .. msg)
end
-- ============================================================
-- SECTION 5: GIT CORE
-- ============================================================
local function git_init_if_needed()
local cmd = string.format("cd %q && git rev-parse --is-inside-work-tree >/dev/null 2>&1", M.world_path)
local is_repo = ie.os.execute(cmd)
if is_repo ~= true and is_repo ~= 0 then
log("action", "No Git repo found — initialising...")
ie.os.execute(string.format(
"cd %q && git init && git add . && git commit -m \"Initial commit\"",
M.world_path
))
end
end
git_init_if_needed()
log("action", string.format("Loaded. Auto-backup every %d seconds.", backup_interval))
function M.do_commit()
if M.world_path == "" then return "skipped" end
ie.os.execute(string.format("rm -f %q/.git/index.lock", M.world_path))
local timestamp = ie.os.date("%Y-%m-%d %H:%M:%S")
local message = string.format("Backup [%s]", timestamp)
local success = ie.os.execute(string.format(
"cd %q && git add . && nice -n 19 ionice -c 3 git commit -m %q",
M.world_path, message
))
if success then
local short_hash = M.shell_exec(string.format(
"cd %q && git rev-parse --short HEAD",
M.world_path
))
log("action", string.format("Snapshot created: %s [%s]", short_hash, timestamp))
return short_hash, timestamp
else
log("action", "No changes detected — snapshot skipped.")
return "skipped"
end
end
local function git_find_hash(hash)
return M.shell_exec(string.format(
"cd %q && git rev-parse --verify %q 2>/dev/null",
M.world_path, hash
))
end
local function git_reset_hard(hash)
ie.os.execute(string.format("cd %q && git reset --hard %s", M.world_path, hash))
end
function M.do_revert(input_hash, commit_time, requester)
local full_hash = git_find_hash(input_hash)
if full_hash == "" then
if requester then
minetest.chat_send_player(requester, M.MOD_TAG .. " Hash '" .. input_hash .. "' not found.")
end
return false
end
local short_hash = full_hash:sub(1, 7)
if not commit_time or commit_time == "" then
commit_time = M.shell_exec(string.format(
"cd %q && git show -s --format='%%ai' %s",
M.world_path, full_hash
))
end
broadcast(string.format(
"WARNING: Server reverting to %s [%s] in %d seconds!",
short_hash, commit_time, M.revert_countdown
))
for i = M.revert_countdown - 1, 1, -1 do
minetest.after(M.revert_countdown - i, function()
broadcast("Reverting in " .. i .. "...")
end)
end
minetest.after(M.revert_countdown, function()
for _, player in ipairs(minetest.get_connected_players()) do
minetest.kick_player(
player:get_player_name(),
string.format("Server reverted to %s [%s]. Please reconnect.", short_hash, commit_time)
)
end
git_reset_hard(full_hash)
minetest.after(0.5, function()
minetest.request_shutdown(
string.format("Revert to %s [%s] complete.", short_hash, commit_time),
false
)
end)
end)
return true
end
-- ============================================================
-- SECTION 6: AUTOMATED BACKUP TIMER
-- ============================================================
minetest.register_globalstep(function(dtime)
backup_timer = backup_timer + dtime
if backup_timer >= backup_interval then
backup_timer = 0
M.do_commit()
end
end)
-- ============================================================
-- SECTION 7: CHAT COMMANDS (/git)
-- ============================================================
local function cmd_commit()
local short_hash, timestamp = M.do_commit()
if short_hash == "skipped" then
return true, "No new changes detected."
end
return true, string.format("Snapshot created — hash: %s [%s]", short_hash, timestamp)
end
local function cmd_log()
local out = M.shell_exec(string.format(
"cd %q && git log --format='%%h | %%ai | %%ar' -n 15",
M.world_path
))
return true, "Last 15 snapshots:\n" .. (out ~= "" and out or "No history found.")
end
local function cmd_revert(args)
local input_hash = args[2]
if not input_hash then
return false, "Usage: /git revert <hash>"
end
local ok = M.do_revert(input_hash, nil, nil)
if not ok then
return false, "Hash '" .. input_hash .. "' not found."
end
return true, string.format(
"Revert scheduled — restoring %s in %d seconds.",
input_hash, M.revert_countdown
)
end
function M.reset_repository()
local cmd = string.format("cd %q && rm -rf .git && git init && git add . && git commit -m \"Reinitialized repository\"", M.world_path)
ie.os.execute(cmd)
return true, "Git repository has been reset and reinitialized."
end
local function cmd_gui(player_name)
local player = minetest.get_player_by_name(player_name)
if not player then return false, "Player not found." end
M.show_gui(player)
return true, ""
end
minetest.register_chatcommand("git", {
params = "<commit|log|revert|gui|rm> [hash]",
description = "Manage world Git snapshots",
privs = { server = true },
func = function(name, param)
local args = {}
for word in param:gmatch("%S+") do
table.insert(args, word)
end
local sub = args[1]
if sub == "commit" or sub == "-c" then return cmd_commit()
elseif sub == "log" or sub == "-l" then return cmd_log()
elseif sub == "revert" or sub == "-r" then return cmd_revert(args)
elseif sub == "gui" or sub == "-g" or sub == "-gui" then return cmd_gui(name)
elseif sub == "-rm" then return M.reset_repository()
else
return true, "Subcommands: commit (-c) | log (-l) | revert (-r) <hash> | gui (-g) | reinitialize git repo (-rm)"
end
end,
})
-- ============================================================
-- SECTION 8: LOAD GUI
-- ============================================================
dofile(minetest.get_modpath("luanti_git_backups") .. "/gui.lua")