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 @ 05b173a7

1
# Copyright (c) 2016, LE GOFF Vincent
2
# Copyright (c) 2016, 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
from urllib.request import urlopen
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
        response = urlopen(url)
86
        try:
87
            info = json.loads(response.read())
88
        except ValueError:
89
            raise UpdateDecodeError
90

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

    
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
        platform = ""
117
        if sys.platform == "win32":
118
            platform = "windows"
119

    
120
        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
                return new_build
126
            else:
127
                raise UknownPlatformUpdateError
128

    
129
        return None
130

    
131
    def download(self, stdout=False):
132
        """Download the build."""
133
        if self.object:
134
            self.object.UpdateText("ui.message.update.downloading")
135
            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
            print("Downloading the build at", self.location)
144

    
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
                print("\r  Downloading... 100%")
178

    
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
        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

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

    
193
        if self.object:
194
            self.object.UpdateText("ui.message.update.extracting")
195
            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
                    print("Extracting {}o".format(total))
215

    
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
                    print("\r  Extracting files... 100%")
237
        except BadZipfile:
238
            raise InvalidSyntaxUpdateError
239

    
240
        # Delete the archive
241
        os.remove(self.path_archive)
242

    
243
        # Move the content of the archive
244
        batch = "timeout 1 /NOBREAK"
245
        for name in os.listdir(os.path.join("updating", "CocoMUD")):
246
            path = os.path.join("updating", "CocoMUD", name)
247
            if os.path.isfile(name):
248
                batch += "\ndel /F /Q " + name
249
                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
            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

    
261

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

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

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

    
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
(3-3/20)