Debug fetching cached repo when verbosity is off
[mdk.git] / mdk / workplace.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 """
5 Moodle Development Kit
6
7 Copyright (c) 2012 Frédéric Massart - FMCorz.net
8
9 This program is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 3 of the License, or
12 (at your option) any later version.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22 http://github.com/FMCorz/mdk
23 """
24
25 import os
26 import shutil
27 import logging
28 from .tools import mkdir, process, stableBranch
29 from .exceptions import CreateException
30 from .config import Conf
31 from . import git
32 from . import moodle
33
34 C = Conf()
35
36
37 class Workplace(object):
38
39 """The name of the directory that contains the PHP files"""
40 wwwDir = None
41
42 """The name of the directory that contains Moodle data"""
43 dataDir = None
44
45 """The name of the directory that contains extra files"""
46 extraDir = None
47
48 """The name of the directory that makes extraDir web accessible, see getMdkWebDir"""
49 mdkDir = None
50
51 """The path to the storage directory"""
52 path = None
53
54 """The path to MDK cache"""
55 cache = None
56
57 """The path to the web accessible directory"""
58 www = None
59
60 def __init__(self, path=None, wwwDir=None, dataDir=None, extraDir=None, mdkDir=None):
61 if path == None:
62 path = C.get('dirs.storage')
63 if wwwDir == None:
64 wwwDir = C.get('wwwDir')
65 if dataDir == None:
66 dataDir = C.get('dataDir')
67 if extraDir == None:
68 extraDir = C.get('extraDir')
69 if mdkDir == None:
70 mdkDir = C.get('mdkDir')
71
72 # Directory paths
73 self.path = os.path.abspath(os.path.realpath(os.path.expanduser(path)))
74 self.cache = os.path.abspath(os.path.realpath(os.path.expanduser(C.get('dirs.mdk'))))
75 self.www = os.path.abspath(os.path.realpath(os.path.expanduser(C.get('dirs.www'))))
76
77 if not os.path.isdir(self.path):
78 raise Exception('Directory %s not found' % self.path)
79
80 # Directory names
81 self.wwwDir = wwwDir
82 self.dataDir = dataDir
83 self.extraDir = extraDir
84 self.mdkDir = mdkDir
85
86 def checkCachedClones(self, stable=True, integration=True):
87 """Clone the official repository in a local cache"""
88 cacheStable = self.getCachedRemote(False)
89 cacheIntegration = self.getCachedRemote(True)
90
91 if not os.path.isdir(cacheStable) and stable:
92 logging.info('Cloning stable repository into cache...')
93
94 # For faster clone, we will copy the integration clone if it exists.
95 if os.path.isdir(cacheIntegration):
96 shutil.copytree(cacheIntegration, cacheStable)
97 repo = git.Git(cacheStable, C.get('git'))
98 repo.setRemote('origin', C.get('remotes.stable'))
99 # The repository is not updated at this stage, it has to be done manually.
100 else:
101 logging.info('This is going to take a while...')
102 process('%s clone --mirror %s %s' % (C.get('git'), C.get('remotes.stable'), cacheStable))
103
104 if not os.path.isdir(cacheIntegration) and integration:
105 logging.info('Cloning integration repository into cache...')
106
107 # For faster clone, we will copy the integration clone if it exists.
108 if os.path.isdir(cacheStable):
109 shutil.copytree(cacheStable, cacheIntegration)
110 repo = git.Git(cacheIntegration, C.get('git'))
111 repo.setRemote('origin', C.get('remotes.integration'))
112 # The repository is not updated at this stage, it has to be done manually.
113 else:
114 logging.info('Have a break, this operation is slow...')
115 process('%s clone --mirror %s %s' % (C.get('git'), C.get('remotes.integration'), cacheIntegration))
116
117 def create(self, name=None, version='master', integration=False, useCacheAsRemote=False):
118 """Creates a new instance of Moodle.
119 The parameter useCacheAsRemote has been deprecated.
120 """
121 if name == None:
122 name = self.generateInstanceName(version, integration=integration)
123
124 if name == self.mdkDir:
125 raise Exception('A Moodle instance cannot be called \'%s\', this is a reserved word.' % self.mdkDir)
126
127 installDir = self.getPath(name)
128 wwwDir = self.getPath(name, 'www')
129 dataDir = self.getPath(name, 'data')
130 extraDir = self.getPath(name, 'extra')
131 linkDir = os.path.join(self.www, name)
132 extraLinkDir = os.path.join(self.getMdkWebDir(), name)
133
134 if self.isMoodle(name):
135 raise CreateException('The Moodle instance %s already exists' % name)
136 elif os.path.isdir(installDir):
137 raise CreateException('Installation path exists: %s' % installDir)
138
139 self.checkCachedClones(not integration, integration)
140 self.updateCachedClones(stable=not integration, integration=integration, verbose=False)
141 mkdir(installDir, 0755)
142 mkdir(wwwDir, 0755)
143 mkdir(dataDir, 0777)
144 mkdir(extraDir, 0777)
145
146 repository = self.getCachedRemote(integration)
147
148 # Clone the instances
149 logging.info('Cloning repository...')
150 process('%s clone %s %s' % (C.get('git'), repository, wwwDir))
151
152 # Symbolic link
153 if os.path.islink(linkDir):
154 os.remove(linkDir)
155 if os.path.isfile(linkDir) or os.path.isdir(linkDir): # No elif!
156 logging.warning('Could not create symbolic link. Please manually create: ln -s %s %s' % (wwwDir, linkDir))
157 else:
158 os.symlink(wwwDir, linkDir)
159
160 # Symlink to extra.
161 if os.path.isfile(extraLinkDir) or os.path.isdir(extraLinkDir):
162 logging.warning('Could not create symbolic link. Please manually create: ln -s %s %s' % (extraDir, extraLinkDir))
163 else:
164 os.symlink(extraDir, extraLinkDir)
165
166 # Symlink to dataDir in wwwDir
167 if type(C.get('symlinkToData')) == str:
168 linkDataDir = os.path.join(wwwDir, C.get('symlinkToData'))
169 if not os.path.isfile(linkDataDir) and not os.path.isdir(linkDataDir) and not os.path.islink(linkDataDir):
170 os.symlink(dataDir, linkDataDir)
171
172 logging.info('Checking out branch...')
173 repo = git.Git(wwwDir, C.get('git'))
174
175 # Removing the default remote origin coming from the clone
176 repo.delRemote('origin')
177
178 # Setting up the correct remote names
179 repo.setRemote(C.get('myRemote'), C.get('remotes.mine'))
180 repo.setRemote(C.get('upstreamRemote'), repository)
181
182 # Creating, fetch, pulling branches
183 repo.fetch(C.get('upstreamRemote'))
184 branch = stableBranch(version)
185 track = '%s/%s' % (C.get('upstreamRemote'), branch)
186 if not repo.hasBranch(branch) and not repo.createBranch(branch, track):
187 logging.error('Could not create branch %s tracking %s' % (branch, track))
188 else:
189 repo.checkout(branch)
190 repo.pull(remote=C.get('upstreamRemote'))
191
192 # Fixing up remote URLs if need be, this is done after pulling the cache one because we
193 # do not want to contact the real origin server from here, it is slow and pointless.
194 if not C.get('useCacheAsUpstreamRemote'):
195 realupstream = C.get('remotes.integration') if integration else C.get('remotes.stable')
196 if realupstream:
197 repo.setRemote(C.get('upstreamRemote'), realupstream)
198
199 M = self.get(name)
200 return M
201
202 def delete(self, name):
203 """Completely remove an instance, database included"""
204
205 # Instantiating the object also checks if it exists
206 M = self.get(name)
207
208 # Deleting the whole thing
209 shutil.rmtree(os.path.join(self.path, name))
210
211 # Deleting the possible symlink
212 link = os.path.join(self.www, name)
213 if os.path.islink(link):
214 try:
215 os.remove(link)
216 except Exception:
217 pass
218
219 # Delete the extra dir symlink
220 link = os.path.join(self.getMdkWebDir(), name)
221 if os.path.islink(link):
222 try:
223 os.remove(link)
224 except Exception:
225 pass
226
227 # Delete db
228 DB = M.dbo()
229 dbname = M.get('dbname')
230 if DB and dbname and DB.dbexists(dbname):
231 DB.dropdb(dbname)
232
233 def generateInstanceName(self, version, integration=False, suffix='', identifier=None):
234 """Creates a name (identifier) from arguments"""
235
236 if identifier != None:
237 # If an identifier is passed, we use it regardless of the other parameters.
238 # Except for suffix.
239 name = identifier.replace(' ', '_')
240 else:
241 # Wording version
242 if version == 'master':
243 prefixVersion = C.get('wording.prefixMaster')
244 else:
245 prefixVersion = version
246
247 # Generating name
248 if integration:
249 name = C.get('wording.prefixIntegration') + prefixVersion
250 else:
251 name = C.get('wording.prefixStable') + prefixVersion
252
253 # Append the suffix
254 if suffix != None and suffix != '':
255 name += C.get('wording.suffixSeparator') + suffix
256
257 return name
258
259 def get(self, name):
260 """Returns an instance defined by its name, or by path"""
261 # Extracts name from path
262 if os.sep in name:
263 path = os.path.abspath(os.path.realpath(name))
264 if not path.startswith(self.path):
265 raise Exception('Could not find Moodle instance at %s' % name)
266 (head, name) = os.path.split(path)
267
268 if not self.isMoodle(name):
269 raise Exception('Could not find Moodle instance %s' % name)
270 return moodle.Moodle(os.path.join(self.path, name, self.wwwDir), identifier=name)
271
272 def getCachedRemote(self, integration=False):
273 """Return the path to the cached remote"""
274 if integration:
275 return os.path.join(self.cache, 'integration.git')
276 else:
277 return os.path.join(self.cache, 'moodle.git')
278
279 def getExtraDir(self, name, subdir=None):
280 """Return the path to the extra directory of an instance
281
282 This also creates the directory if does not exist.
283 """
284 path = self.getPath(name, 'extra')
285 if subdir:
286 path = os.path.join(path, subdir)
287 if not os.path.exists(path):
288 mkdir(path, 0777)
289 return path
290
291 def getMdkWebDir(self):
292 """Return (and create) the special MDK web directory."""
293 mdkExtra = os.path.join(self.www, self.mdkDir)
294 if not os.path.exists(mdkExtra):
295 mkdir(mdkExtra, 0777)
296
297 return mdkExtra
298
299 def getPath(self, name, mode=None):
300 """Returns the path of an instance base on its name"""
301 base = os.path.join(self.path, name)
302 if mode == 'www':
303 return os.path.join(base, self.wwwDir)
304 elif mode == 'data':
305 return os.path.join(base, self.dataDir)
306 elif mode == 'extra':
307 return os.path.join(base, self.extraDir)
308 else:
309 return base
310
311 def getUrl(self, name, extra=None):
312 """Return the URL to an instance, or to its extra directory if extra is passed"""
313 base = '%s://%s' % (C.get('scheme'), C.get('host'))
314
315 if C.get('path') != '' and C.get('path') != None:
316 base = '%s/%s' % (base, C.get('path'))
317
318 wwwroot = None
319 if not extra:
320 wwwroot = '%s/%s' % (base, name)
321 else:
322 wwwroot = '%s/%s/%s/%s' % (base, self.mdkDir, name, extra)
323
324 return wwwroot
325
326 def isMoodle(self, name):
327 """Checks whether a Moodle instance exist under this name"""
328 d = os.path.join(self.path, name)
329 if not os.path.isdir(d):
330 return False
331
332 wwwDir = os.path.join(d, self.wwwDir)
333 dataDir = os.path.join(d, self.dataDir)
334 if not os.path.isdir(wwwDir) or not os.path.isdir(dataDir):
335 return False
336
337 if not moodle.Moodle.isInstance(wwwDir):
338 return False
339
340 return True
341
342 def list(self, integration=None, stable=None):
343 """Return the list of Moodle instances"""
344 dirs = os.listdir(self.path)
345 names = []
346 for d in dirs:
347 if d == '.' or d == '..': continue
348 if not os.path.isdir(os.path.join(self.path, d)): continue
349 if not self.isMoodle(d): continue
350 if integration != None or stable != None:
351 M = self.get(d)
352 if not integration and M.isIntegration(): continue
353 if not stable and M.isStable(): continue
354 names.append(d)
355 return names
356
357 def resolve(self, name=None, path=None):
358 """Try to find a Moodle instance based on its name, a path or the working directory"""
359
360 # A name was passed, is that a valid instance?
361 if name != None:
362 if self.isMoodle(name):
363 return self.get(name)
364 return None
365
366 # If a path was not passed, let's use the current working directory.
367 if path == None:
368 path = os.getcwd()
369 path = os.path.realpath(os.path.abspath(path))
370
371 # Is this path in a Moodle instance?
372 if path.startswith(self.path):
373
374 # Get the relative path identifier/some/other/path
375 relative = os.path.relpath(path, self.path)
376
377 # Isolating the identifier, it should be the first directory
378 (head, tail) = os.path.split(relative)
379 while head:
380 (head, tail) = os.path.split(head)
381
382 if self.isMoodle(tail):
383 return self.get(tail)
384
385 return False
386
387 def resolveMultiple(self, names=[]):
388 """Return multiple instances"""
389 if type(names) != list:
390 if type(names) == str:
391 names = list(names)
392 else:
393 raise Exception('Unexpected variable type')
394
395 # Nothing has been passed, we use resolve()
396 if len(names) < 1:
397 M = self.resolve()
398 if M:
399 return [M]
400 else:
401 return []
402
403 # Try to resolve each instance
404 result = []
405 for name in names:
406 M = self.resolve(name=name)
407 if M:
408 result.append(M)
409 else:
410 logging.info('Could not find instance called %s' % name)
411 return result
412
413 def updateCachedClones(self, integration=True, stable=True, verbose=True):
414 """Update the cached clone of the repositories"""
415
416 caches = []
417
418 if integration:
419 caches.append(os.path.join(self.cache, 'integration.git'))
420 if stable:
421 caches.append(os.path.join(self.cache, 'moodle.git'))
422
423 for cache in caches:
424 if not os.path.isdir(cache):
425 continue
426
427 repo = git.Git(cache, C.get('git'))
428
429 if verbose:
430 logging.info('Fetching cached repository %s...', os.path.basename(cache))
431 else:
432 logging.debug('Fetching cached repository %s...', os.path.basename(cache))
433 if not repo.fetch():
434 raise Exception('Could not fetch in repository %s' % (cache))
435
436 return True