Project

Profile

Help

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

github / src / autoupdate.py @ 96664dff

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
import sys
36
from threading import Thread
37
from urllib2 import urlopen
38
from zipfile import ZipFile, BadZipfile
39

    
40
class AutoUpdate(Thread):
41

    
42
    """Class to perform an automatic update.
43

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

49
    """
50

    
51
    def __init__(self, current_version, object=None, just_checking=False):
52
        Thread.__init__(self)
53
        if isinstance(current_version, basestring):
54
            if not current_version.isdigit():
55
                raise ValueError("the current version {} isn't an int".format(
56
                        repr(current_version)))
57
            else:
58
                current_version = int(current_version)
59

    
60
        self.current_version = current_version
61
        self.location = None
62
        self.path_archive = None
63
        self.object = object
64
        self.just_checking = just_checking
65

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

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

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

    
83
        url = "https://cocomud.plan.io/projects/cocomud-client.json"
84
        response = urlopen(url)
85
        try:
86
            info = json.loads(response.read())
87
        except ValueError:
88
            raise UpdateDecodeError
89

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

    
97
        # If a recent build has been found, try to read it
98
        if recent_build:
99
            try:
100
                recent_build = json.loads(recent_build)
101
            except ValueError:
102
                raise UpdateDecodeError
103
        else:
104
            raise UnavailableUpdateError
105

    
106
        # If everything went according to plan, recent_build is a dictionary
107
        # with build: {locations}
108
        new_build = list(recent_build.keys())[0]
109
        if new_build.isdigit():
110
            new_build = int(new_build)
111
        else:
112
            raise InvalidSyntaxUpdateError
113

    
114
        # If the recent build is greated than the current one
115
        platform = ""
116
        if sys.platform == "win32":
117
            platform = "windows"
118

    
119
        location = recent_build[str(new_build)].get(platform)
120
        if location:
121
            self.location = location
122
        if new_build > self.current_version:
123
            if self.location:
124
                return new_build
125
            else:
126
                raise UknownPlatformUpdateError
127

    
128
        return None
129

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

    
136
        # Creates a new folder for updates
137
        if os.path.exists("updating"):
138
            shutil.rmtree("updating")
139

    
140
        os.mkdir("updating")
141
        if stdout:
142
            print "Downloading the build at", self.location
143

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

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

    
169
                chunk = response.read(chunk_size)
170
                if not chunk:
171
                    keep = False
172

    
173
                file.write(chunk)
174

    
175
            if stdout:
176
                print "\r  Downloading... 100%"
177

    
178
        self.path_archive = path_archive
179

    
180
    def update(self, stdout=False):
181
        """Update the archive.
182

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

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

    
192
        if self.object:
193
            self.object.UpdateText("ui.message.update.extracting")
194
            self.object.UpdateGauge(0)
195

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

    
208
                    total += info.file_size
209
                    extract.append(info)
210

    
211
                # Extract these files
212
                if stdout:
213
                    print "Extracting {}o".format(total)
214

    
215
                extracted = 0.0
216
                if stdout:
217
                    sys.stdout.write("  Extracting files...   0%")
218
                    sys.stdout.flush()
219

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

    
231
                    archive.extract(info.filename, "updating")
232
                    extracted += info.file_size
233

    
234
                if stdout:
235
                    print "\r  Extracting files... 100%"
236
        except BadZipfile:
237
            raise InvalidSyntaxUpdateError
238

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

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

    
260

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

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

    
274
        if self.object:
275
            self.object.AskDestroy()
276
        os.startfile("bgupdating.bat")
277
        sys.exit(0)
278

    
279

    
280
# Exceptions
281
class UpdateError(RuntimeError):
282

    
283
    """An error occured dduring the update."""
284

    
285
    pass
286

    
287

    
288
class UpdateDecodeError(UpdateError):
289

    
290
    """The update couldn't be decoded."""
291

    
292
    pass
293

    
294

    
295
class UnavailableUpdateError(UpdateError):
296

    
297
    """The update cannot be reached."""
298

    
299
    pass
300

    
301

    
302
class InvalidSyntaxUpdateError(UpdateError):
303

    
304
    """Something went wrong in the syntax of the build."""
305

    
306
    pass
307

    
308

    
309
class UknownPlatformUpdateError(UpdateError):
310

    
311
    """No platform could be found for this build."""
312

    
313
    pass
(3-3/19)