/stdhome

To get this branch, use:
bzr branch http://bzr.ed.am/stdhome
2 by Tim Marston
added global objects (the.repo, the.program), deployment object and implemented
1
# deployment.py
2
#
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
3
# Copyright (C) 2013 to 2014 Tim Marston <tim@edm.am>
2 by Tim Marston
added global objects (the.repo, the.program), deployment object and implemented
4
#
5
# This file is part of stdhome (hereafter referred to as "this program").
6
# See http://ed.am/dev/stdhome for more information.
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
# GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
22
import os, re, shutil, filecmp
23
import the, util
2 by Tim Marston
added global objects (the.repo, the.program), deployment object and implemented
24
25
26
class Deployment:
27
28
29
	def __init__( self ):
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
30
		self.load_deployment_state()
31
		self.conflicts_checked = False
32
33
34
	def load_deployment_state( self ):
35
36
		# list of files that were copied-in (or at least given the opportunity
37
        # to be) and updated through the vcs update.  This means that, while
38
        # there may have been conflicts during the update (which will be dealt
39
        # with in the repo), any arising conflicts with the filesystem are now
40
        # assumed to be because of the vcs update and can be ignored (that is to
41
        # say, we no longer care about the file in the home directory).  In
42
        # short, this is a list of files that it is safe to deploy, regardless
43
        # of the state of the filesystem.
44
		self.deploy_files = None
45
46
		# do we have a repo?
47
		if not os.path.exists( the.repo.full_dir ): return
48
49
		# if no file list exists, we're done
50
		file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
51
		if not os.path.isfile( file ):
52
			if the.verbose: print "no deployment state found"
53
			return
54
55
		# read the file list
56
		if the.verbose: print "loading deployment state"
57
		f = open( file, 'r' )
58
		self.deploy_files = f.read().splitlines()
59
60
61
	def save_deployment_state( self ):
62
		if the.verbose: print "saving deployment state"
63
64
		# create metadata directory, as necessary
65
		if not os.path.isdir( the.full_mddir ):
66
			os.mkdir( the.full_mddir )
67
68
		# create file
69
		file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
70
		f = open( file, 'w' )
71
		f.write( '\n'.join( self.deploy_files ) + '\n' )
72
73
74
	def remove_deployment_state( self ):
75
76
		# delete it, if it exists
77
		file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
78
		if( os.path.isfile( file ) ):
79
			os.unlink( file )
80
81
82
	def is_ongoing( self ):
83
		return False if self.deploy_files is None else True
84
85
86
	def check_ongoing( self, ongoing = True ):
87
		if( ongoing ):
88
			if self.deploy_files is None:
89
				raise self.DeploymentOngoing( False )
2 by Tim Marston
added global objects (the.repo, the.program), deployment object and implemented
90
		else:
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
91
			if self.deploy_files is not None:
92
				raise self.DeploymentOngoing( True )
93
94
95
	def confirm( self, message ):
96
		# TODO: write interactive confirmation routine
97
		return False
98
99
100
	def walk_repo( self, dir_func = None, file_func = None, link_func = None,
101
				   relative_dir = '', ignore_errors = False ):
102
		"""Walk through all files and directories in the repo, passing the relative path
103
		to each to the provided functions.
104
		"""
105
		full_dir = os.path.join( the.repo.full_dir, relative_dir )
106
		for file in os.listdir( full_dir ):
107
			relative_file = os.path.join( relative_dir, file )
108
109
			# skip some stuff
110
			if relative_file == '.bzr': continue
111
112
			repo_file = os.path.join( the.repo.full_dir, relative_file )
113
			fs_file = os.path.join( the.full_fsdir, relative_file )
114
			if os.path.islink( repo_file ):
115
				if link_func is not None:
116
					link_func( relative_file, repo_file, fs_file )
117
			elif os.path.isfile( repo_file ):
118
				if file_func is not None:
119
					file_func( relative_file, repo_file, fs_file )
120
			elif os.path.isdir( repo_file ):
121
				if dir_func is not None:
122
					dir_func( relative_path, repo_file, fs_file )
123
				# recurse in directories
124
				self.walk_repo( dir_func, file_func, link_func, relative_dir,
125
								ignore_errors )
126
			elif not ignore_errors:
127
				raise RuntimeError(
128
					'repo contains unknown/missing entity' )
2 by Tim Marston
added global objects (the.repo, the.program), deployment object and implemented
129
130
131
	def copy_in( self ):
132
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
133
		def copy_in_dir( relative_file, dst, src ):
134
			self.deploy_files.append( relative_file )
135
136
			# if src doesn't exist, delete it from the repo
137
			if not os.path.lexists( src ):
138
				if the.verbose: print " _<d " + relative_file
139
				shutil.rmtree( dst )
140
141
			# if src is a directory, copy permissions, as necessary
142
			elif os.path.isdir( src ):
143
				# TODO: should check permissions and only do as necessary
144
				if the.verbose: print " d<d " + relative_file
145
				shutil.copystat( src, dst )
146
147
			# TODO: serious differences in between ~/ and repo (e.g., files in
148
			# one that are directories in the other) should be ignored (e.g.,
149
			# not copied-in).  And the stuff that is ignored during copy-in
150
			# should also be ignored during copy-out and must not be added to
151
			# the deploy_files list.  Since these ignored files/directories are
152
			# transparent to the user, they should have to explicitly permit
153
			# them via an ignore file (e.g., ~/.stdhome/.ignore, akin to bzr's
154
			# .bzrignore file).  If these serious differences are not matched by
155
			# the ignore file, an error should show (which will requie a
156
			# separate "check" walk of the repo, as is done in copy_out).
157
			else:
158
				raise self.Conflict( "%s differs too severely from the repo" %
159
									 os.path.join( the.fsdir, relative_file ) )
160
161
		def copy_in_file( relative_file, dst, src ):
162
			self.deploy_files.append( relative_file )
163
164
			# if src doesn't exist, delete it in the repo
165
			if not os.path.lexists( src ):
166
				if the.verbose: print " ?<f " + relative_file
167
				os.unlink( dst )
168
169
			# if src is a symlink, replace it in repo
170
			elif os.path.islink( src ):
171
				if the.verbose: print " l<f " + relative_file
172
				os.unlink( dst )
173
				os.symlink( os.readlink( src ), dst )
174
175
			# if src is a file, replace it if different
176
			elif os.path.isfile( src ):
177
				if not filecmp.cmp( src, dst ):
178
					if the.verbose: print " f<f " + relative_file
179
					os.unlink( dst )
180
					shutil.copy( src, dst )
181
					shutil.copystat( src, dst )
182
				else:
183
					if the.verbose: print " f=f " + relative_file
184
185
			# TODO: serious differences in between ~/ and repo (e.g., files in
186
			# one that are directories in the other) should be ignored (e.g.,
187
			# not copied-in).  And the stuff that is ignored during copy-in
188
			# should also be ignored during copy-out and must not be added to
189
			# the deploy_files list.  Since these ignored files/directories are
190
			# transparent to the user, they should have to explicitly permit
191
			# them via an ignore file (e.g., ~/.stdhome/.ignore, akin to bzr's
192
			# .bzrignore file).  If these serious differences are not matched by
193
			# the ignore file, an error should show (which will requie a
194
			# separate "check" walk of the repo, as is done in copy_out).
195
			else:
196
				raise self.Conflict( "%s differs too severely from the repo" %
197
									 os.path.join( the.fsdir, relative_file ) )
198
199
		def copy_in_link( relative_file, dst, src ):
200
			self.deploy_files.append( relative_file )
201
202
			# if src doesn't exist, delete it in the repo
203
			if not os.path.lexists( src ):
204
				if the.verbose: print " _<l " + relative_file
205
				os.unlink( dst )
206
207
			# if src is a symlink, replace it if different in repo
208
			elif os.path.islink( src ):
209
				if os.readlink( src ) != os.readlink( dst ):
210
					if the.verbose: print " l<l " + relative_file
211
					os.unlink( dst )
212
					os.symlink( os.readlink( src ), dst )
213
				else:
214
					if the.verbose: print " l=l " + relative_file
215
216
			# if src is a file, replace it in repo
217
			elif os.path.isfile( src ):
218
				if the.verbose: print " f<l " + relative_file
219
				os.unlink( dst )
220
				shutil.copy( src, dst )
221
				shutil.copystat( src, dst )
222
223
			# TODO: serious differences in between ~/ and repo (e.g., files in
224
			# one that are directories in the other) should be ignored (e.g.,
225
			# not copied-in).  And the stuff that is ignored during copy-in
226
			# should also be ignored during copy-out and must not be added to
227
			# the deploy_files list.  Since these ignored files/directories are
228
			# transparent to the user, they should have to explicitly permit
229
			# them via an ignore file (e.g., ~/.stdhome/.ignore, akin to bzr's
230
			# .bzrignore file).  If these serious differences are not matched by
231
			# the ignore file, an error should show (which will requie a
232
			# separate "check" walk of the repo, as is done in copy_out).
233
			else:
234
				raise self.Conflict( "%s differs too severely from the repo" %
235
									 os.path.join( the.fsdir, relative_file ) )
236
2 by Tim Marston
added global objects (the.repo, the.program), deployment object and implemented
237
		# check we don't already have a file list
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
238
		self.check_ongoing( False )
239
240
		# new file list
241
		self.deploy_files = list()
242
243
		# if the repo doesn't exist, we're done
244
		if not os.path.exists( the.repo.full_dir ): return
245
246
		# copy in
247
		if the.verbose: print "importing files"
248
		self.walk_repo( dir_func = copy_in_dir, file_func = copy_in_file,
249
					   link_func = copy_in_link, ignore_errors = True )
250
251
		# save state
252
		self.save_deployment_state()
2 by Tim Marston
added global objects (the.repo, the.program), deployment object and implemented
253
254
255
	def copy_out( self ):
3 by Tim Marston
added bzr as a vcs backend; finished init command; implemented deployment
256
257
		def copy_out_dir( relative_file, src, dst ):
258
259
			# if dst doesn't exist, create it
260
			if not os.path.lexists( dst ):
261
				if the.verbose: print " d>_ " + relative_file
262
				os.mkdir( dst )
263
				shutil.copystat( src, dst )
264
265
			# if dst is a file/symlink, replace it
266
			elif os.path.isfile( dst ):
267
				if the.verbose: print " d>f " + relative_file
268
				os.unlink( dst )
269
				os.mkdir( dst )
270
				shutil.copystat( src, dst )
271
272
			# if dst is a directory, copy permissions as required
273
			elif os.path.isdir( dst ):
274
				# TODO: should check permission and only do as necessary
275
				if the.verbose: print " d>d " + relative_file
276
				shutil.copystat( src, dst )
277
278
			else:
279
				raise NotImplementedError()
280
281
		def copy_out_file( relative_file, src, dst ):
282
283
			# if dst doesn't exist, copy
284
			if not os.path.lexists( dst ):
285
				if the.verbose: print " f>_ " + relative_file
286
287
				shutil.copy( src, dst )
288
				shutil.copystat( src, dst )
289
290
			# if dst is a symlink, replace it
291
			elif os.path.islink( dst ):
292
				if the.verbose: print " f>l " + relative_file
293
				os.unlink( dst )
294
				shutil.copy( src, dst )
295
				shutil.copystat( src, dst )
296
297
			# if dst is a file, replace it if different
298
			elif os.path.isfile( dst ):
299
				if not filecmp.cmp( src, dst ):
300
					if the.verbose: print " f>f " + relative_file
301
					os.unlink( dst )
302
					shutil.copy( src, dst )
303
					shutil.copystat( src, dst )
304
				else:
305
					if the.verbose: print " f=f " + relative_file
306
307
			# if dst is a directory, replace it
308
			elif os.path.isdir( dst ):
309
				if the.verbose: print " f>d " + relative_file
310
				shutil.rmtree( dst )
311
				shutil.copy( src, dst )
312
				shutil.copystat( src, dst )
313
314
			else:
315
				raise NotImplementedError()
316
317
		def copy_out_link( relative_file, src, dst ):
318
319
			# if dst doesn't exist, copy
320
			if not os.path.lexists( dst ):
321
				if the.verbose: print " l>_ " + relative_file
322
				os.symlink( os.readlink( src ), dst )
323
324
			# if dst is a symlink, replace it if different
325
			elif os.path.islink( dst ):
326
				if os.readlink( src ) != os.readlink( dst ):
327
					if the.verbose: print " l>l " + relative_file
328
					os.unlink( dst )
329
					os.symlink( os.readlink( src ), dst )
330
				else:
331
					if the.verbose: print " l=l " + relative_file
332
333
			# if dst is a file, replace it
334
			elif os.path.isfile( dst ):
335
				if the.verbose: print " l>f " + relative_file
336
				os.unlink( dst )
337
				os.symlink( os.readlink( src ), dst )
338
339
			# if dst is a directory, replace it
340
			elif os.path.isdir( dst ):
341
				if the.verbose: print " l>d " + relative_file
342
				shutil.rmtree( dst )
343
				os.symlink( os.readlink( src ), dst )
344
345
			else:
346
				raise NotImplementedError()
347
348
		# check we have a file list
349
		self.check_ongoing( True )
350
351
		# we should already have handled conflicts
352
		if not self.conflicts_checked:
353
			raise RuntimeError(
354
				'logic error: conflicts should have been checked!' )
355
356
		# copy out
357
		if the.verbose: print "exporting files"
358
		self.walk_repo( dir_func = copy_out_dir, file_func = copy_out_file,
359
					   link_func = copy_out_link, ignore_errors = True )
360
361
		# clear state
362
		self.remove_deployment_state()
363
364
365
	def get_conflicts( self ):
366
367
		def check_dir( relative_file, src, dst ):
368
369
			# files that existed at copy-in can be copied-out and overwritten
370
			if self.deploy_files is not None and \
371
			   relative_file in self.deploy_files: return
372
373
			# files that don't exist in the filesystem can be copied-out
374
			if not os.path.lexists( dst ): return
375
376
			# accept/reject existing files
377
			if os.path.islink( dst ):
378
				self.files.append( "%s already exists (as a symlink)" %
379
								   os.path.join( the.fsdir, relative_file ) )
380
			elif os.path.isfile( dst ):
381
				self.files.append( "%s already exists (as a file)" %
382
								   os.path.join( the.fsdir, relative_file ) )
383
			elif os.path.isdir( dst ):
384
				return
385
			else:
386
				self.files.append( "%s already exists" %
387
								   os.path.join( the.fsdir, relative_file ) )
388
389
		def check_file( relative_file, src, dst ):
390
391
			# files that existed at copy-in can be copied-out and overwritten
392
			if self.deploy_files is not None and \
393
			   relative_file in self.deploy_files: return
394
395
			# files that don't exist in the filesystem can be copied-out
396
			if not os.path.lexists( dst ): return
397
398
			# accept/reject existing files
399
			if os.path.isfile( dst ):
400
				self.files.append( "%s already exists" %
401
								   os.path.join( the.fsdir, relative_file ) )
402
			elif os.path.isdir( dst ):
403
				self.files.append( "%s already exists (as a directory)" %
404
								   os.path.join( the.fsdir, relative_file ) )
405
			else:
406
				self.files.append( "%s already exists" %
407
								   os.path.join( the.fsdir, relative_file ) )
408
409
		self.conflicts_checked = True
410
411
		# check for conflicts
412
		self.files = list()
413
		self.walk_repo( dir_func = check_dir, file_func = check_file,
414
					   link_func = check_file )
415
		return self.files
416
417
418
	class DeploymentOngoing( the.program.FatalError ):
419
420
		def __init__( self, ongoing ):
421
			if( ongoing ):
422
				self.msg = "there is an ongoing deployment"
423
			else:
424
				self.msg = "there is no ongoing deployment"
425
426
427
	class Conflict( Exception ):
428
429
		def __init__( self, message ):
430
			self.msg = message