Project

Profile

Help

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

github / src / autoupdate.py @ master

1
# Copyright (c) 2016-2020, LE GOFF Vincent
2
# Copyright (c) 2016-2020, LE GOFF Vincent
3
# 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
from subprocess import Popen
36
import sys
37
from threading import Thread
38
import urllib.error, urllib.request
39
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
    def __init__(self, current_version, object=None, just_checking=False):
53
        Thread.__init__(self)
54
        if isinstance(current_version, str):
55
            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
        self.just_checking = just_checking
66

    
67
    def run(self):
68
        """Run the thread."""
69
        build = self.check()
70
        if self.object:
71
            self.object.ResponseUpdate(build)
72

    
73
        if build is not None:
74
            if not self.just_checking:
75
                self.download()
76
                self.update()
77

    
78
    def check(self):
79
        """Check for updates."""
80
        if self.object:
81
            self.object.UpdateText("ui.message.update.checking")
82
            self.object.UpdateGauge(0)
83

    
84
        url = "https://cocomud.plan.io/projects/cocomud-client.json"
85
        try:
86
            response = urllib.request.urlopen(url)
87
        except urllib.error.URLError:
88
            return
89

    
90
        try:
91
            info = json.loads(response.read())
92
        except ValueError:
93
            raise UpdateDecodeError
94

    
95
        # The latest builds are in a custom field
96
        customs = info.get("project", {}).get("custom_fields")
97
        recent_build = None
98
        for field in customs:
99
            if field['name'] == "build":
100
                recent_build = field['value']
101

    
102
        # If a recent build has been found, try to read it
103
        if recent_build:
104
            try:
105
                recent_build = json.loads(recent_build)
106
            except ValueError:
107
                raise UpdateDecodeError
108
        else:
109
            raise UnavailableUpdateError
110

    
111
        # If everything went according to plan, recent_build is a dictionary
112
        # with build: {locations}
113
        new_build = list(recent_build.keys())[0]
114
        if new_build.isdigit():
115
            new_build = int(new_build)
116
        else:
117
            raise InvalidSyntaxUpdateError
118

    
119
        # If the recent build is greated than the current one
120
        platform = ""
121
        if sys.platform == "win32":
122
            platform = "windows"
123

    
124
        location = recent_build[str(new_build)].get(platform)
125
        if location:
126
            self.location = location
127
        if new_build > self.current_version:
128
            if self.location:
129
                return new_build
130
            else:
131
                raise UknownPlatformUpdateError
132

    
133
    def download(self, stdout=False):
134
        """Download the build."""
135
        if self.object:
136
            self.object.UpdateText("ui.message.update.downloading")
137
            self.object.UpdateGauge(0)
138

    
139
        # Creates a new folder for updates
140
        if os.path.exists("updating"):
141
            shutil.rmtree("updating")
142

    
143
        os.mkdir("updating")
144
        if stdout:
145
            print("Downloading the build at", self.location)
146

    
147
        # Get the build
148
        response = urllib.request.urlopen(self.location)
149
        meta = response.info()
150
        size = int(meta["Content-Length"])
151
        chunk_size = 4096
152
        path_archive = os.path.join("updating", "build.zip")
153
        with open(path_archive, "wb") as file:
154
            keep = True
155
            progress = 0.0
156
            percent = 0
157
            if stdout:
158
                sys.stdout.write("  Downloading...   0.0%")
159
                sys.stdout.flush()
160

    
161
            while keep:
162
                old_percent = percent
163
                progress += chunk_size
164
                percent = round((progress / size) * 100, 1)
165
                if self.object and int(percent) != int(old_percent):
166
                    self.object.UpdateGauge(int(percent))
167
                elif old_percent != percent and stdout:
168
                    sys.stdout.write("\r  Downloading... {:>5}%".format(
169
                            percent))
170
                    sys.stdout.flush()
171

    
172
                chunk = response.read(chunk_size)
173
                if not chunk:
174
                    keep = False
175

    
176
                file.write(chunk)
177

    
178
            if stdout:
179
                print("\r  Downloading... 100%")
180

    
181
        self.path_archive = path_archive
182

    
183
    def update(self, stdout=False):
184
        """Update the archive.
185

186
        This method must be called after downloading the new archive.
187
        Since it cannot perform all updates by itself (it would delete
188
        the updater and a couple of libraries), it needs to pass a batch
189
        file at the end that will delete itself.
190

191
        """
192
        if self.path_archive is None:
193
            raise ValueError("the updated archive hasn't been downloaded")
194

    
195
        if self.object:
196
            self.object.UpdateText("ui.message.update.extracting")
197
            self.object.UpdateGauge(0)
198

    
199
        # Analyze the zip file
200
        try:
201
            with ZipFile(self.path_archive, "r") as archive:
202
                infos = archive.infolist()
203
                extract = []
204
                total = 0
205
                for info in infos:
206
                    name = info.filename
207
                    names = name.split("/")
208
                    if len(names) > 1 and names[1] in ("settings", "worlds"):
209
                        continue
210

    
211
                    total += info.file_size
212
                    extract.append(info)
213

    
214
                # Extract these files
215
                if stdout:
216
                    print("Extracting {}o".format(total))
217

    
218
                extracted = 0.0
219
                if stdout:
220
                    sys.stdout.write("  Extracting files...   0%")
221
                    sys.stdout.flush()
222

    
223
                percent = 0.0
224
                for info in extract:
225
                    old_percent = percent
226
                    percent = round(extracted / total * 100, 1)
227
                    if self.object and int(percent) != int(old_percent):
228
                        self.object.UpdateGauge(int(percent))
229
                    elif old_percent != percent and stdout:
230
                        sys.stdout.write("\r  Extracting files..." \
231
                                "   {:>5}%".format(percent))
232
                        sys.stdout.flush()
233

    
234
                    archive.extract(info.filename, "updating")
235
                    extracted += info.file_size
236

    
237
                if stdout:
238
                    print("\r  Extracting files... 100%")
239
        except BadZipfile:
240
            raise InvalidSyntaxUpdateError
241

    
242
        # Delete the archive
243
        os.remove(self.path_archive)
244

    
245
        # Move the content of the archive
246
        batch = "timeout 1 /NOBREAK"
247
        for name in os.listdir(os.path.join("updating", "CocoMUD")):
248
            path = os.path.join("updating", "CocoMUD", name)
249
            if os.path.isfile(name):
250
                batch += "\ndel /F /Q " + name
251
                batch += "\ncopy /V " + path + " " + name
252
            elif os.path.isdir(name):
253
                batch += "\nrmdir /Q /S " + name
254
                batch += "\nmd " + name
255
                batch += "\nxcopy /S " + path + " " + name
256
            elif not os.path.exists(name):
257
                if os.path.isfile(path):
258
                    batch += "\ncopy /V " + path + " " + name
259
                elif os.path.isdir(path):
260
                    batch += "\nmd " + name
261
                    batch += "\nxcopy /S " + path + " " + name
262

    
263

    
264
        # Add instructions to delete the clean update
265
        batch += "\nrmdir /S /Q updating"
266
        batch += "\nstart /B \"\" cocomud.exe"
267
        batch += "\nexit"
268

    
269
        # Write the batch file
270
        with open("updating.bat", "w") as file:
271
            file.write(batch)
272
        with open("bgupdating.bat", "w") as file:
273
            cmd = "cmd /C updating.bat >> update.log 2>&1"
274
            cmd += "exit"
275
            file.write(cmd)
276

    
277
        if self.object:
278
            self.object.AskDestroy()
279
        #os.startfile("bgupdating.bat")
280
        hidden = 0x08000000 # Windows only
281
        Popen(["bgupdating.bat"], bufsize=-1, creationflags=hidden)
282
        sys.exit(0)
283

    
284

    
285
# Exceptions
286
class UpdateError(RuntimeError):
287

    
288
    """An error occured dduring the update."""
289

    
290
    pass
291

    
292

    
293
class UpdateDecodeError(UpdateError):
294

    
295
    """The update couldn't be decoded."""
296

    
297
    pass
298

    
299

    
300
class UnavailableUpdateError(UpdateError):
301

    
302
    """The update cannot be reached."""
303

    
304
    pass
305

    
306

    
307
class InvalidSyntaxUpdateError(UpdateError):
308

    
309
    """Something went wrong in the syntax of the build."""
310

    
311
    pass
312

    
313

    
314
class UknownPlatformUpdateError(UpdateError):
315

    
316
    """No platform could be found for this build."""
317

    
318
    pass
(3-3/23)