3
# Copyright (C) 2013 to 2014 Tim Marston <tim@edm.am>
5
# This file is part of stdhome (hereafter referred to as "this program").
6
# See http://ed.am/dev/stdhome for more information.
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.
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.
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/>.
22
import os, re, shutil, filecmp
30
self.load_deployment_state()
31
self.conflicts_checked = False
34
def load_deployment_state( self ):
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
47
if not os.path.exists( the.repo.full_dir ): return
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"
56
if the.verbose: print "loading deployment state"
58
self.deploy_files = f.read().splitlines()
61
def save_deployment_state( self ):
62
if the.verbose: print "saving deployment state"
64
# create metadata directory, as necessary
65
if not os.path.isdir( the.full_mddir ):
66
os.mkdir( the.full_mddir )
69
file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
71
f.write( '\n'.join( self.deploy_files ) + '\n' )
74
def remove_deployment_state( self ):
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 ) ):
82
def is_ongoing( self ):
83
return False if self.deploy_files is None else True
86
def check_ongoing( self, ongoing = True ):
88
if self.deploy_files is None:
89
raise self.DeploymentOngoing( False )
91
if self.deploy_files is not None:
92
raise self.DeploymentOngoing( True )
95
def confirm( self, message ):
96
# TODO: write interactive confirmation routine
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.
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 )
110
if relative_file == '.bzr': continue
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,
126
elif not ignore_errors:
128
'repo contains unknown/missing entity' )
133
def copy_in_dir( relative_file, dst, src ):
134
self.deploy_files.append( relative_file )
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
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 )
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).
158
raise self.Conflict( "%s differs too severely from the repo" %
159
os.path.join( the.fsdir, relative_file ) )
161
def copy_in_file( relative_file, dst, src ):
162
self.deploy_files.append( relative_file )
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
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
173
os.symlink( os.readlink( src ), dst )
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
180
shutil.copy( src, dst )
181
shutil.copystat( src, dst )
183
if the.verbose: print " f=f " + relative_file
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).
196
raise self.Conflict( "%s differs too severely from the repo" %
197
os.path.join( the.fsdir, relative_file ) )
199
def copy_in_link( relative_file, dst, src ):
200
self.deploy_files.append( relative_file )
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
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
212
os.symlink( os.readlink( src ), dst )
214
if the.verbose: print " l=l " + relative_file
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
220
shutil.copy( src, dst )
221
shutil.copystat( src, dst )
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).
234
raise self.Conflict( "%s differs too severely from the repo" %
235
os.path.join( the.fsdir, relative_file ) )
237
# check we don't already have a file list
238
self.check_ongoing( False )
241
self.deploy_files = list()
243
# if the repo doesn't exist, we're done
244
if not os.path.exists( the.repo.full_dir ): return
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 )
252
self.save_deployment_state()
255
def copy_out( self ):
257
def copy_out_dir( relative_file, src, dst ):
259
# if dst doesn't exist, create it
260
if not os.path.lexists( dst ):
261
if the.verbose: print " d>_ " + relative_file
263
shutil.copystat( src, dst )
265
# if dst is a file/symlink, replace it
266
elif os.path.isfile( dst ):
267
if the.verbose: print " d>f " + relative_file
270
shutil.copystat( src, dst )
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 )
279
raise NotImplementedError()
281
def copy_out_file( relative_file, src, dst ):
283
# if dst doesn't exist, copy
284
if not os.path.lexists( dst ):
285
if the.verbose: print " f>_ " + relative_file
287
shutil.copy( src, dst )
288
shutil.copystat( src, dst )
290
# if dst is a symlink, replace it
291
elif os.path.islink( dst ):
292
if the.verbose: print " f>l " + relative_file
294
shutil.copy( src, dst )
295
shutil.copystat( src, dst )
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
302
shutil.copy( src, dst )
303
shutil.copystat( src, dst )
305
if the.verbose: print " f=f " + relative_file
307
# if dst is a directory, replace it
308
elif os.path.isdir( dst ):
309
if the.verbose: print " f>d " + relative_file
311
shutil.copy( src, dst )
312
shutil.copystat( src, dst )
315
raise NotImplementedError()
317
def copy_out_link( relative_file, src, dst ):
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 )
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
329
os.symlink( os.readlink( src ), dst )
331
if the.verbose: print " l=l " + relative_file
333
# if dst is a file, replace it
334
elif os.path.isfile( dst ):
335
if the.verbose: print " l>f " + relative_file
337
os.symlink( os.readlink( src ), dst )
339
# if dst is a directory, replace it
340
elif os.path.isdir( dst ):
341
if the.verbose: print " l>d " + relative_file
343
os.symlink( os.readlink( src ), dst )
346
raise NotImplementedError()
348
# check we have a file list
349
self.check_ongoing( True )
351
# we should already have handled conflicts
352
if not self.conflicts_checked:
354
'logic error: conflicts should have been checked!' )
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 )
362
self.remove_deployment_state()
365
def get_conflicts( self ):
367
def check_dir( relative_file, src, dst ):
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
373
# files that don't exist in the filesystem can be copied-out
374
if not os.path.lexists( dst ): return
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 ):
386
self.files.append( "%s already exists" %
387
os.path.join( the.fsdir, relative_file ) )
389
def check_file( relative_file, src, dst ):
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
395
# files that don't exist in the filesystem can be copied-out
396
if not os.path.lexists( dst ): return
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 ) )
406
self.files.append( "%s already exists" %
407
os.path.join( the.fsdir, relative_file ) )
409
self.conflicts_checked = True
411
# check for conflicts
413
self.walk_repo( dir_func = check_dir, file_func = check_file,
414
link_func = check_file )
418
class DeploymentOngoing( the.program.FatalError ):
420
def __init__( self, ongoing ):
422
self.msg = "there is an ongoing deployment"
424
self.msg = "there is no ongoing deployment"
427
class Conflict( Exception ):
429
def __init__( self, message ):