forked from PathOfBuildingCommunity/PathOfBuilding
-
Notifications
You must be signed in to change notification settings - Fork 0
/
UpdateCheck.lua
287 lines (272 loc) · 9.11 KB
/
UpdateCheck.lua
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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
#@
-- Path of Building
--
-- Module: Update Check
-- Checks for updates
--
local proxyURL = ...
local xml = require("xml")
local sha1 = require("sha1")
local curl = require("lcurl.safe")
local lzip = require("lzip")
local globalRetryLimit = 10
local function downloadFileText(url)
for i = 1, 5 do
if i > 1 then
ConPrintf("Retrying... (%d of 5)", i)
end
local text = ""
local easy = curl.easy()
easy:setopt_url(url)
easy:setopt(curl.OPT_ACCEPT_ENCODING, "")
if proxyURL then
easy:setopt(curl.OPT_PROXY, proxyURL)
end
easy:setopt_writefunction(function(data)
text = text..data
return true
end)
local _, error = easy:perform()
easy:close()
if not error then
return text
end
ConPrintf("Download failed (%s)", error:msg())
if globalRetryLimit == 0 or i == 5 then
return nil, error:msg()
end
globalRetryLimit = globalRetryLimit - 1
end
end
local function downloadFile(url, outName)
local text = downloadFileText(url)
if text then
local outFile = io.open(outName, "wb")
outFile:write(text)
outFile:close()
else
return true
end
end
ConPrintf("Checking for update...")
local scriptPath = GetScriptPath()
local runtimePath = GetRuntimePath()
-- Load and process local manifest
local localVer
local localPlatform, localBranch
local localFiles = { }
local localManXML = xml.LoadXMLFile(scriptPath.."/manifest.xml")
local localSource
local runtimeExecutable
if localManXML and localManXML[1].elem == "PoBVersion" then
for _, node in ipairs(localManXML[1]) do
if type(node) == "table" then
if node.elem == "Version" then
localVer = node.attrib.number
localPlatform = node.attrib.platform
localBranch = node.attrib.branch
elseif node.elem == "Source" then
if node.attrib.part == "program" then
localSource = node.attrib.url
end
elseif node.elem == "File" then
local fullPath
if node.attrib.part == "runtime" then
fullPath = runtimePath .. "/" .. node.attrib.name
else
fullPath = scriptPath .. "/" .. node.attrib.name
end
localFiles[node.attrib.name] = { sha1 = node.attrib.sha1, part = node.attrib.part, platform = node.attrib.platform, fullPath = fullPath }
if node.attrib.part == "runtime" and node.attrib.name:match("Path of Building") then
runtimeExecutable = fullPath
end
end
end
end
end
if not localVer or not localSource or not localBranch or not next(localFiles) then
ConPrintf("Update check failed: invalid local manifest")
return nil, "Invalid local manifest"
end
localSource = localSource:gsub("{branch}", localBranch)
-- Download and process remote manifest
local remoteVer
local remoteFiles = { }
local remoteSources = { }
local remoteManText, errMsg = downloadFileText(localSource.."manifest.xml")
if not remoteManText then
ConPrintf("Update check failed: couldn't download version manifest")
return nil, "Couldn't download version manifest.\nReason: "..errMsg.."\nCheck your internet connectivity.\nIf you are using a proxy, specify it in Options."
end
local remoteManXML = xml.ParseXML(remoteManText)
if remoteManXML and remoteManXML[1].elem == "PoBVersion" then
for _, node in ipairs(remoteManXML[1]) do
if type(node) == "table" then
if node.elem == "Version" then
remoteVer = node.attrib.number
elseif node.elem == "Source" then
if not remoteSources[node.attrib.part] then
remoteSources[node.attrib.part] = { }
end
remoteSources[node.attrib.part][node.attrib.platform or "any"] = node.attrib.url
elseif node.elem == "File" then
if not node.attrib.platform or node.attrib.platform == localPlatform then
local fullPath
if node.attrib.part == "runtime" then
fullPath = runtimePath .. "/" .. node.attrib.name
else
fullPath = scriptPath .. "/" .. node.attrib.name
end
remoteFiles[node.attrib.name] = { sha1 = node.attrib.sha1, part = node.attrib.part, platform = node.attrib.platform, fullPath = fullPath }
end
end
end
end
end
if not remoteVer or not next(remoteSources) or not next(remoteFiles) then
ConPrintf("Update check failed: invalid remote manifest")
return nil, "Invalid remote manifest"
end
-- Build lists of files to be updated or deleted
local updateFiles = { }
for name, data in pairs(remoteFiles) do
data.name = name
if not localFiles[name] or localFiles[name].sha1 ~= data.sha1 then
table.insert(updateFiles, data)
elseif localFiles[name] then
local file = io.open(localFiles[name].fullPath, "rb")
if not file then
ConPrintf("Warning: '%s' doesn't exist, it will be re-downloaded", data.name)
table.insert(updateFiles, data)
else
local content = file:read("*a")
file:close()
if data.sha1 ~= sha1(content) and data.sha1 ~= sha1(content:gsub("\n","\r\n")) then
ConPrintf("Warning: Integrity check on '%s' failed, it will be replaced", data.name)
table.insert(updateFiles, data)
end
end
end
end
local deleteFiles = { }
for name, data in pairs(localFiles) do
data.name = name
if not remoteFiles[name] then
table.insert(deleteFiles, data)
end
end
if #updateFiles == 0 and #deleteFiles == 0 then
ConPrintf("No update available.")
return "none"
end
MakeDir("Update")
ConPrintf("Downloading update...")
-- Download changelog
downloadFile(localSource.."changelog.txt", scriptPath.."/changelog.txt")
-- Download files that need updating
local failedFile = false
local zipFiles = { }
for index, data in ipairs(updateFiles) do
if UpdateProgress then
UpdateProgress("Downloading %d/%d", index, #updateFiles)
end
local partSources = remoteSources[data.part]
local source = partSources[localPlatform] or partSources["any"]
source = source:gsub("{branch}", localBranch)
local fileName = scriptPath.."/Update/"..data.name:gsub("[\\/]","{slash}")
data.updateFileName = fileName
local content
local zipName = source:match("/([^/]+%.zip)$")
if zipName then
if not zipFiles[zipName] then
ConPrintf("Downloading %s...", zipName)
local zipFileName = scriptPath.."/Update/"..zipName
downloadFile(source, zipFileName)
zipFiles[zipName] = lzip.open(zipFileName)
end
local zip = zipFiles[zipName]
if zip then
local zippedFile = zip:OpenFile(data.name)
if zippedFile then
content = zippedFile:Read("*a")
zippedFile:Close()
else
ConPrintf("Couldn't extract '%s' from '%s' (extract failed)", data.name, zipName)
end
else
ConPrintf("Couldn't extract '%s' from '%s' (zip open failed)", data.name, zipName)
end
else
ConPrintf("Downloading %s... (%d of %d)", data.name, index, #updateFiles)
content = downloadFileText(source..data.name)
end
if content then
if data.sha1 ~= sha1(content) and data.sha1 ~= sha1(content:gsub("\n","\r\n")) then
ConPrintf("Hash mismatch on '%s'", data.name)
failedFile = true
else
local file = io.open(fileName, "w+b")
file:write(content)
file:close()
end
else
failedFile = true
end
end
for name, zip in pairs(zipFiles) do
zip:Close()
os.remove(scriptPath.."/Update/"..name)
end
if failedFile then
ConPrintf("Update failed: one or more files couldn't be downloaded")
return nil, "One or more files couldn't be downloaded.\nCheck your internet connectivity,\nor try again later."
end
-- Create new manifest
localManXML = { elem = "PoBVersion" }
table.insert(localManXML, { elem = "Version", attrib = { number = remoteVer, platform = localPlatform, branch = localBranch } })
for part, platforms in pairs(remoteSources) do
for platform, url in pairs(platforms) do
table.insert(localManXML, { elem = "Source", attrib = { part = part, platform = platform ~= "any" and platform, url = url } })
end
end
for name, data in pairs(remoteFiles) do
table.insert(localManXML, { elem = "File", attrib = { name = data.name, sha1 = data.sha1, part = data.part, platform = data.platform } })
end
xml.SaveXMLFile(localManXML, scriptPath.."/Update/manifest.xml")
-- Build list of operations to apply the update
local updateMode = "normal"
local ops = { }
local opsRuntime = { }
for _, data in pairs(updateFiles) do
-- Ensure that the destination path of this file exists
local dirStr = ""
for dir in data.fullPath:gmatch("([^/]+/)") do
dirStr = dirStr .. dir
MakeDir(dirStr)
end
if data.part == "runtime" then
-- Core runtime file, will need to update from the basic environment
-- These files will be updated on the second pass of the update script, with the first pass being run within the normal environment
updateMode = "basic"
table.insert(opsRuntime, 'move "'..data.updateFileName..'" "'..data.fullPath..'"')
else
table.insert(ops, 'move "'..data.updateFileName..'" "'..data.fullPath..'"')
end
end
for _, data in pairs(deleteFiles) do
table.insert(ops, 'delete "'..data.fullPath..'"')
end
table.insert(ops, 'move "'..scriptPath..'/Update/manifest.xml" "'..scriptPath..'/manifest.xml"')
if updateMode == "basic" then
-- Update script will need to relaunch the normal environment after updating
table.insert(opsRuntime, 'start "'..runtimeExecutable..'"')
local opRuntimeFile = io.open(scriptPath.."/Update/opFileRuntime.txt", "w+")
opRuntimeFile:write(table.concat(opsRuntime, "\n"))
opRuntimeFile:close()
end
-- Write operations file
local opFile = io.open(scriptPath.."/Update/opFile.txt", "w+")
opFile:write(table.concat(ops, "\n"))
opFile:close()
ConPrintf("Update is ready.")
return updateMode