Changes to instance naming:
[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', purpose='stable', engine=C.get('defaultEngine'), useCacheAsRemote=False):
118 """Creates a new instance of Moodle.
119 The parameter useCacheAsRemote has been deprecated.
120 """
121 integration = False
122
123 if name == None:
124 name = self.generateInstanceName(version, purpose=purpose)
125
126 if name == self.mdkDir:
127 raise Exception('A Moodle instance cannot be called \'%s\', this is a reserved word.' % self.mdkDir)
128
129 if purpose == 'integration':
130 integration = True
131
132 installDir = self.getPath(name)
133 wwwDir = self.getPath(name, 'www')
134 dataDir = self.getPath(name, 'data')
135 extraDir = self.getPath(name, 'extra')
136 linkDir = os.path.join(self.www, name)
137 extraLinkDir = os.path.join(self.getMdkWebDir(), name)
138
139 if self.isMoodle(name):
140 raise CreateException('The Moodle instance %s already exists' % name)
141 elif os.path.isdir(installDir):
142 raise CreateException('Installation path exists: %s' % installDir)
143
144 self.checkCachedClones(not integration, integration)
145 self.updateCachedClones(stable=not integration, integration=integration, verbose=False)
146 mkdir(installDir, 0755)
147 mkdir(wwwDir, 0755)
148 mkdir(dataDir, 0777)
149 mkdir(extraDir, 0777)
150
151 repository = self.getCachedRemote(integration)
152
153 # Clone the instances
154 logging.info('Cloning repository...')
155 process('%s clone %s %s' % (C.get('git'), repository, wwwDir))
156
157 # Symbolic link
158 if os.path.islink(linkDir):
159 os.remove(linkDir)
160 if os.path.isfile(linkDir) or os.path.isdir(linkDir): # No elif!
161 logging.warning('Could not create symbolic link. Please manually create: ln -s %s %s' % (wwwDir, linkDir))
162 else:
163 os.symlink(wwwDir, linkDir)
164
165 # Symlink to extra.
166 if os.path.isfile(extraLinkDir) or os.path.isdir(extraLinkDir):
167 logging.warning('Could not create symbolic link. Please manually create: ln -s %s %s' % (extraDir, extraLinkDir))
168 else:
169 os.symlink(extraDir, extraLinkDir)
170
171 # Symlink to dataDir in wwwDir
172 if type(C.get('symlinkToData')) == str:
173 linkDataDir = os.path.join(wwwDir, C.get('symlinkToData'))
174 if not os.path.isfile(linkDataDir) and not os.path.isdir(linkDataDir) and not os.path.islink(linkDataDir):
175 os.symlink(dataDir, linkDataDir)
176
177 logging.info('Checking out branch...')
178 repo = git.Git(wwwDir, C.get('git'))
179
180 # Removing the default remote origin coming from the clone
181 repo.delRemote('origin')
182
183 # Setting up the correct remote names
184 repo.setRemote(C.get('myRemote'), C.get('remotes.mine'))
185 repo.setRemote(C.get('upstreamRemote'), repository)
186
187 # Creating, fetch, pulling branches
188 repo.fetch(C.get('upstreamRemote'))
189 branch = stableBranch(version)
190 track = '%s/%s' % (C.get('upstreamRemote'), branch)
191 if not repo.hasBranch(branch) and not repo.createBranch(branch, track):
192 logging.error('Could not create branch %s tracking %s' % (branch, track))
193 else:
194 repo.checkout(branch)
195 repo.pull(remote=C.get('upstreamRemote'))
196
197 # Fixing up remote URLs if need be, this is done after pulling the cache one because we
198 # do not want to contact the real origin server from here, it is slow and pointless.
199 if not C.get('useCacheAsUpstreamRemote'):
200 realupstream = C.get('remotes.integration') if integration else C.get('remotes.stable')
201 if realupstream:
202 repo.setRemote(C.get('upstreamRemote'), realupstream)
203
204 M = self.get(name)
205 return M
206
207 def delete(self, name):
208 """Completely remove an instance, database included"""
209
210 # Instantiating the object also checks if it exists
211 M = self.get(name)
212
213 # Deleting the whole thing
214 shutil.rmtree(os.path.join(self.path, name))
215
216 # Deleting the possible symlink
217 link = os.path.join(self.www, name)
218 if os.path.islink(link):
219 try:
220 os.remove(link)
221 except Exception:
222 pass
223
224 # Delete the extra dir symlink
225 link = os.path.join(self.getMdkWebDir(), name)
226 if os.path.islink(link):
227 try:
228 os.remove(link)
229 except Exception:
230 pass
231
232 # Delete db
233 DB = M.dbo()
234 dbname = M.get('dbname')
235 if DB and dbname and DB.dbexists(dbname):
236 DB.dropdb(dbname)
237
238 def generateInstanceName(self, version, engine=C.get('defaultEngine'), purpose='stable', suffix='', identifier=None):
239 """Creates a name (identifier) from arguments"""
240
241 if identifier != None:
242 # If an identifier is passed, we use it regardless of the other parameters.
243 # Except for suffix.
244 name = identifier.replace(' ', '_')
245 else:
246 # Wording version
247 if version == 'master':
248 prefixVersion = C.get('wording.prefixMaster')
249 else:
250 prefixVersion = version
251
252 # Generating name
253 sep = C.get('wording.prefixSeparator');
254 if purpose == 'integration':
255 name = C.get('wording.prefixIntegration') + sep + prefixVersion
256
257 if purpose == 'review':
258 name = C.get('wording.prefixReview') + sep + prefixVersion
259
260 if purpose == 'stable':
261 name = C.get('wording.prefixStable') + sep + prefixVersion
262
263 if C.get('wording.appendEngine'):
264 name += sep + engine
265
266 # Append the suffix
267 if suffix != None and suffix != '':
268 name += C.get('wording.suffixSeparator') + suffix
269
270 return name
271
272 def get(self, name):
273 """Returns an instance defined by its name, or by path"""
274 # Extracts name from path
275 if os.sep in name:
276 path = os.path.abspath(os.path.realpath(name))
277 if not path.startswith(self.path):
278 raise Exception('Could not find Moodle instance at %s' % name)
279 (head, name) = os.path.split(path)
280
281 if not self.isMoodle(name):
282 raise Exception('Could not find Moodle instance %s' % name)
283 return moodle.Moodle(os.path.join(self.path, name, self.wwwDir), identifier=name)
284
285 def getCachedRemote(self, integration=False):
286 """Return the path to the cached remote"""
287 if integration:
288 return os.path.join(self.cache, 'integration.git')
289 else:
290 return os.path.join(self.cache, 'moodle.git')
291
292 def getExtraDir(self, name, subdir=None):
293 """Return the path to the extra directory of an instance
294
295 This also creates the directory if does not exist.
296 """
297 path = self.getPath(name, 'extra')
298 if subdir:
299 path = os.path.join(path, subdir)
300 if not os.path.exists(path):
301 mkdir(path, 0777)
302 return path
303
304 def getMdkWebDir(self):
305 """Return (and create) the special MDK web directory."""
306 mdkExtra = os.path.join(self.www, self.mdkDir)
307 if not os.path.exists(mdkExtra):
308 mkdir(mdkExtra, 0777)
309
310 return mdkExtra
311
312 def getPath(self, name, mode=None):
313 """Returns the path of an instance base on its name"""
314 base = os.path.join(self.path, name)
315 if mode == 'www':
316 return os.path.join(base, self.wwwDir)
317 elif mode == 'data':
318 return os.path.join(base, self.dataDir)
319 elif mode == 'extra':
320 return os.path.join(base, self.extraDir)
321 else:
322 return base
323
324 def getUrl(self, name, extra=None):
325 """Return the URL to an instance, or to its extra directory if extra is passed"""
326 base = '%s://%s' % (C.get('scheme'), C.get('host'))
327
328 if C.get('path') != '' and C.get('path') != None:
329 base = '%s/%s' % (base, C.get('path'))
330
331 wwwroot = None
332 if not extra:
333 wwwroot = '%s/%s' % (base, name)
334 else:
335 wwwroot = '%s/%s/%s/%s' % (base, self.mdkDir, name, extra)
336
337 return wwwroot
338
339 def isMoodle(self, name):
340 """Checks whether a Moodle instance exist under this name"""
341 d = os.path.join(self.path, name)
342 if not os.path.isdir(d):
343 return False
344
345 wwwDir = os.path.join(d, self.wwwDir)
346 dataDir = os.path.join(d, self.dataDir)
347 if not os.path.isdir(wwwDir) or not os.path.isdir(dataDir):
348 return False
349
350 if not moodle.Moodle.isInstance(wwwDir):
351 return False
352
353 return True
354
355 def list(self, integration=None, stable=None):
356 """Return the list of Moodle instances"""
357 dirs = os.listdir(self.path)
358 names = []
359 for d in dirs:
360 if d == '.' or d == '..': continue
361 if not os.path.isdir(os.path.join(self.path, d)): continue
362 if not self.isMoodle(d): continue
363 if integration != None or stable != None:
364 M = self.get(d)
365 if not integration and M.isIntegration(): continue
366 if not stable and M.isStable(): continue
367 names.append(d)
368 return names
369
370 def resolve(self, name=None, path=None):
371 """Try to find a Moodle instance based on its name, a path or the working directory"""
372
373 # A name was passed, is that a valid instance?
374 if name != None:
375 if self.isMoodle(name):
376 return self.get(name)
377 return None
378
379 # If a path was not passed, let's use the current working directory.
380 if path == None:
381 path = os.getcwd()
382 path = os.path.realpath(os.path.abspath(path))
383
384 # Is this path in a Moodle instance?
385 if path.startswith(self.path):
386
387 # Get the relative path identifier/some/other/path
388 relative = os.path.relpath(path, self.path)
389
390 # Isolating the identifier, it should be the first directory
391 (head, tail) = os.path.split(relative)
392 while head:
393 (head, tail) = os.path.split(head)
394
395 if self.isMoodle(tail):
396 return self.get(tail)
397
398 return False
399
400 def resolveMultiple(self, names=[]):
401 """Return multiple instances"""
402 if type(names) != list:
403 if type(names) == str:
404 names = list(names)
405 else:
406 raise Exception('Unexpected variable type')
407
408 # Nothing has been passed, we use resolve()
409 if len(names) < 1:
410 M = self.resolve()
411 if M:
412 return [M]
413 else:
414 return []
415
416 # Try to resolve each instance
417 result = []
418 for name in names:
419 M = self.resolve(name=name)
420 if M:
421 result.append(M)
422 else:
423 logging.info('Could not find instance called %s' % name)
424 return result
425
426 def updateCachedClones(self, integration=True, stable=True, verbose=True):
427 """Update the cached clone of the repositories"""
428
429 caches = []
430
431 if integration:
432 caches.append(os.path.join(self.cache, 'integration.git'))
433 if stable:
434 caches.append(os.path.join(self.cache, 'moodle.git'))
435
436 for cache in caches:
437 if not os.path.isdir(cache):
438 continue
439
440 repo = git.Git(cache, C.get('git'))
441
442 if verbose:
443 logging.info('Fetching cached repository %s...', os.path.basename(cache))
444 else:
445 logging.debug('Fetching cached repository %s...', os.path.basename(cache))
446 if not repo.fetch():
447 raise Exception('Could not fetch in repository %s' % (cache))
448
449 return True