Project

Profile

Help

Download (10.5 KB) Statistics View on GitHub Reload from mirrored respository
| Branch: | Tag: | Revision:

github / src / autoupdate.py @ 05b173a7

1 8ca46736 Vincent Le Goff
# Copyright (c) 2016, LE GOFF Vincent
2 5148ac31 Vincent Le Goff
# Copyright (c) 2016, LE GOFF Vincent
3 8ca46736 Vincent Le Goff
# All rights reserved.
4
5
# Redistribution and use in source and binary forms, with or without
6
# modification, are permitted provided that the following conditions are met:
7
8
# * Redistributions of source code must retain the above copyright notice, this
9
#   list of conditions and the following disclaimer.
10
11
# * Redistributions in binary form must reproduce the above copyright notice,
12
#   this list of conditions and the following disclaimer in the documentation
13
#   and/or other materials provided with the distribution.
14
15
# * Neither the name of ytranslate nor the names of its
16
#   contributors may be used to endorse or promote products derived from
17
#   this software without specific prior written permission.
18
19
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30
"""This file contains the AutoUpdate class."""
31
32
import json
33
import os
34
import shutil
35 8f68a7b0 Vincent Le Goff
from subprocess import Popen
36 8ca46736 Vincent Le Goff
import sys
37
from threading import Thread
38 8f68a7b0 Vincent Le Goff
from urllib.request import urlopen
39 8ca46736 Vincent Le Goff
from zipfile import ZipFile, BadZipfile
40
41
class AutoUpdate(Thread):
42
43
    """Class to perform an automatic update.
44

45
    An object of this class is used to perform an automatic update.
46
    This can be triggered by a standalone program (required when the
47
    update actually performs), but some methods of its object can
48
    be used to check for updates and do not leave the application.
49

50
    """
51
52 5148ac31 Vincent Le Goff
    def __init__(self, current_version, object=None, just_checking=False):
53 8ca46736 Vincent Le Goff
        Thread.__init__(self)
54 8f68a7b0 Vincent Le Goff
        if isinstance(current_version, str):
55 8ca46736 Vincent Le Goff
            if not current_version.isdigit():
56
                raise ValueError("the current version {} isn't an int".format(
57
                        repr(current_version)))
58
            else:
59
                current_version = int(current_version)
60
61
        self.current_version = current_version
62
        self.location = None
63
        self.path_archive = None
64
        self.object = object
65 5148ac31 Vincent Le Goff
        self.just_checking = just_checking
66 8ca46736 Vincent Le Goff
67
    def run(self):
68
        """Run the thread."""
69 5148ac31 Vincent Le Goff
        build = self.check()
70 50456a44 Vincent Le Goff
        if self.object:
71
            self.object.ResponseUpdate(build)
72 5148ac31 Vincent Le Goff
73 50456a44 Vincent Le Goff
        if build is not None:
74 5148ac31 Vincent Le Goff
            if not self.just_checking:
75
                self.download()
76
                self.update()
77 8ca46736 Vincent Le Goff
78
    def check(self):
79
        """Check for updates."""
80
        if self.object:
81 e97bfeca Vincent Le Goff
            self.object.UpdateText("ui.message.update.checking")
82 8ca46736 Vincent Le Goff
            self.object.UpdateGauge(0)
83
84 0df353d7 Vincent Le Goff
        url = "https://cocomud.plan.io/projects/cocomud-client.json"
85
        response = urlopen(url)
86
        try:
87
            info = json.loads(response.read())
88
        except ValueError:
89
            raise UpdateDecodeError
90 8ca46736 Vincent Le Goff
91
        # The latest builds are in a custom field
92 0df353d7 Vincent Le Goff
        customs = info.get("project", {}).get("custom_fields")
93 8ca46736 Vincent Le Goff
        recent_build = None
94
        for field in customs:
95 0df353d7 Vincent Le Goff
            if field['name'] == "build":
96
                recent_build = field['value']
97 8ca46736 Vincent Le Goff
98
        # If a recent build has been found, try to read it
99
        if recent_build:
100
            try:
101
                recent_build = json.loads(recent_build)
102
            except ValueError:
103
                raise UpdateDecodeError
104
        else:
105
            raise UnavailableUpdateError
106
107
        # If everything went according to plan, recent_build is a dictionary
108
        # with build: {locations}
109
        new_build = list(recent_build.keys())[0]
110
        if new_build.isdigit():
111
            new_build = int(new_build)
112
        else:
113
            raise InvalidSyntaxUpdateError
114
115
        # If the recent build is greated than the current one
116 70b69697 Vincent Le Goff
        platform = ""
117
        if sys.platform == "win32":
118
            platform = "windows"
119 8ca46736 Vincent Le Goff
120 70b69697 Vincent Le Goff
        location = recent_build[str(new_build)].get(platform)
121
        if location:
122
            self.location = location
123
        if new_build > self.current_version:
124
            if self.location:
125 5148ac31 Vincent Le Goff
                return new_build
126 8ca46736 Vincent Le Goff
            else:
127
                raise UknownPlatformUpdateError
128
129 5148ac31 Vincent Le Goff
        return None
130 8ca46736 Vincent Le Goff
131
    def download(self, stdout=False):
132
        """Download the build."""
133
        if self.object:
134 e97bfeca Vincent Le Goff
            self.object.UpdateText("ui.message.update.downloading")
135 8ca46736 Vincent Le Goff
            self.object.UpdateGauge(0)
136
137
        # Creates a new folder for updates
138
        if os.path.exists("updating"):
139
            shutil.rmtree("updating")
140
141
        os.mkdir("updating")
142
        if stdout:
143 8f68a7b0 Vincent Le Goff
            print("Downloading the build at", self.location)
144 8ca46736 Vincent Le Goff
145
        # Get the build
146
        response = urlopen(self.location)
147
        meta = response.info()
148
        size = int(meta.getheaders("Content-Length")[0])
149
        chunk_size = 4096
150
        path_archive = os.path.join("updating", "build.zip")
151
        with open(path_archive, "wb") as file:
152
            keep = True
153
            progress = 0.0
154
            percent = 0
155
            if stdout:
156
                sys.stdout.write("  Downloading...   0.0%")
157
                sys.stdout.flush()
158
159
            while keep:
160
                old_percent = percent
161
                progress += chunk_size
162
                percent = round((progress / size) * 100, 1)
163
                if self.object and int(percent) != int(old_percent):
164
                    self.object.UpdateGauge(int(percent))
165
                elif old_percent != percent and stdout:
166
                    sys.stdout.write("\r  Downloading... {:>5}%".format(
167
                            percent))
168
                    sys.stdout.flush()
169
170
                chunk = response.read(chunk_size)
171
                if not chunk:
172
                    keep = False
173
174
                file.write(chunk)
175
176
            if stdout:
177 8f68a7b0 Vincent Le Goff
                print("\r  Downloading... 100%")
178 8ca46736 Vincent Le Goff
179
        self.path_archive = path_archive
180
181
    def update(self, stdout=False):
182
        """Update the archive.
183

184
        This method must be called after downloading the new archive.
185 0df353d7 Vincent Le Goff
        Since it cannot perform all updates by itself (it would delete
186
        the updater and a couple of libraries), it needs to pass a batch
187
        file at the end that will delete itself.
188 8ca46736 Vincent Le Goff

189
        """
190
        if self.path_archive is None:
191
            raise ValueError("the updated archive hasn't been downloaded")
192
193
        if self.object:
194 e97bfeca Vincent Le Goff
            self.object.UpdateText("ui.message.update.extracting")
195 8ca46736 Vincent Le Goff
            self.object.UpdateGauge(0)
196
197
        # Analyze the zip file
198
        try:
199
            with ZipFile(self.path_archive, "r") as archive:
200
                infos = archive.infolist()
201
                extract = []
202
                total = 0
203
                for info in infos:
204
                    name = info.filename
205
                    names = name.split("/")
206
                    if len(names) > 1 and names[1] in ("settings", "worlds"):
207
                        continue
208
209
                    total += info.file_size
210
                    extract.append(info)
211
212
                # Extract these files
213
                if stdout:
214 8f68a7b0 Vincent Le Goff
                    print("Extracting {}o".format(total))
215 8ca46736 Vincent Le Goff
216
                extracted = 0.0
217
                if stdout:
218
                    sys.stdout.write("  Extracting files...   0%")
219
                    sys.stdout.flush()
220
221
                percent = 0.0
222
                for info in extract:
223
                    old_percent = percent
224
                    percent = round(extracted / total * 100, 1)
225
                    if self.object and int(percent) != int(old_percent):
226
                        self.object.UpdateGauge(int(percent))
227
                    elif old_percent != percent and stdout:
228
                        sys.stdout.write("\r  Extracting files..." \
229
                                "   {:>5}%".format(percent))
230
                        sys.stdout.flush()
231
232
                    archive.extract(info.filename, "updating")
233
                    extracted += info.file_size
234
235
                if stdout:
236 8f68a7b0 Vincent Le Goff
                    print("\r  Extracting files... 100%")
237 8ca46736 Vincent Le Goff
        except BadZipfile:
238
            raise InvalidSyntaxUpdateError
239
240 0df353d7 Vincent Le Goff
        # Delete the archive
241
        os.remove(self.path_archive)
242
243 8ca46736 Vincent Le Goff
        # Move the content of the archive
244 0df353d7 Vincent Le Goff
        batch = "timeout 1 /NOBREAK"
245 8ca46736 Vincent Le Goff
        for name in os.listdir(os.path.join("updating", "CocoMUD")):
246
            path = os.path.join("updating", "CocoMUD", name)
247 8281b4d3 Vincent Le Goff
            if os.path.isfile(name):
248 0df353d7 Vincent Le Goff
                batch += "\ndel /F /Q " + name
249 8281b4d3 Vincent Le Goff
                batch += "\ncopy /V " + path + " " + name
250
            elif os.path.isdir(name):
251
                batch += "\nrmdir /Q /S " + name
252
                batch += "\nmd " + name
253
                batch += "\nxcopy /S " + path + " " + name
254 38f7eb08 Vincent Le Goff
            elif not os.path.exists(name):
255
                if os.path.isfile(path):
256
                    batch += "\ncopy /V " + path + " " + name
257
                elif os.path.isdir(path):
258
                    batch += "\nmd " + name
259
                    batch += "\nxcopy /S " + path + " " + name
260 0df353d7 Vincent Le Goff
261 8ca46736 Vincent Le Goff
262 0df353d7 Vincent Le Goff
        # Add instructions to delete the clean update
263 8281b4d3 Vincent Le Goff
        batch += "\nrmdir /S /Q updating"
264 5148ac31 Vincent Le Goff
        batch += "\nstart /B \"\" cocomud.exe"
265
        batch += "\nexit"
266 8ca46736 Vincent Le Goff
267 0df353d7 Vincent Le Goff
        # Write the batch file
268
        with open("updating.bat", "w") as file:
269
            file.write(batch)
270 8281b4d3 Vincent Le Goff
        with open("bgupdating.bat", "w") as file:
271 5148ac31 Vincent Le Goff
            cmd = "cmd /C updating.bat >> update.log 2>&1"
272
            cmd += "exit"
273 8281b4d3 Vincent Le Goff
            file.write(cmd)
274 8ca46736 Vincent Le Goff
275 0df353d7 Vincent Le Goff
        if self.object:
276
            self.object.AskDestroy()
277 dd86a0be Vincent Le Goff
        #os.startfile("bgupdating.bat")
278
        hidden = 0x08000000 # Windows only
279
        Popen(["bgupdating.bat"], bufsize=-1, creationflags=hidden)
280 0df353d7 Vincent Le Goff
        sys.exit(0)
281 8ca46736 Vincent Le Goff
282
283
# Exceptions
284
class UpdateError(RuntimeError):
285
286
    """An error occured dduring the update."""
287
288
    pass
289
290
291
class UpdateDecodeError(UpdateError):
292
293
    """The update couldn't be decoded."""
294
295
    pass
296
297
298
class UnavailableUpdateError(UpdateError):
299
300
    """The update cannot be reached."""
301
302
    pass
303
304
305
class InvalidSyntaxUpdateError(UpdateError):
306
307
    """Something went wrong in the syntax of the build."""
308
309
    pass
310
311
312
class UknownPlatformUpdateError(UpdateError):
313
314
    """No platform could be found for this build."""
315
316
    pass