drupid 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,683 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Copyright (c) 2012 Lifepillar
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ module Drupid
24
+ # Helper class to build or update a Drupal installation.
25
+ class Updater
26
+ include Drupid::Utils
27
+
28
+ # A Drupid::Makefile object.
29
+ attr :makefile
30
+ # A Drupid::Platform object.
31
+ attr :platform
32
+ # (For multisite platforms) the site to be synchronized.
33
+ attr :site
34
+ # The updater's log.
35
+ attr :log
36
+
37
+ # Creates a new updater for a given makefile and a given platform.
38
+ # For multisite platforms, optionally specify a site to synchronize.
39
+ def initialize(makefile, platform, site_name = nil)
40
+ @makefile = makefile
41
+ @platform = platform
42
+ @site = site_name
43
+ @log = Log.new
44
+ #
45
+ @libraries_paths = Array.new
46
+ @core_projects = Array.new
47
+ @derivative_builds = Array.new
48
+ @excluded_projects = Hash.new
49
+ end
50
+
51
+ # Returns a list of names of projects that must be considered
52
+ # as processed when synchronizing. This always include all
53
+ # Drupal core projects, but other projects may be added.
54
+ #
55
+ # Requires: this updater's platform must have been analyzed
56
+ # (see Drupid::Platform.analyze).
57
+ def excluded
58
+ @excluded_projects.keys
59
+ end
60
+
61
+ # Adds the given list of project names to the exclusion set of this updater.
62
+ def exclude(project_list)
63
+ project_list.each { |p| @excluded_projects[p] = true }
64
+ end
65
+
66
+ # Returns true if the given project is in the exclusion set of this updater;
67
+ # returns false otherwise.
68
+ def excluded?(project_name)
69
+ @excluded_projects.has_key?(project_name)
70
+ end
71
+
72
+ # Returns true if there are actions that have not been
73
+ # applied to the platform (including actions in derivative builds);
74
+ # returns false otherwise.
75
+ def pending_actions?
76
+ return true if @log.pending_actions?
77
+ @derivative_builds.each do |d|
78
+ return true if d.pending_actions?
79
+ end
80
+ return false
81
+ end
82
+
83
+ # Enqueues a derivative build based on the specified project
84
+ # (which is typically, but not necessarily, an installation profile).
85
+ # Does nothing if the project does not contain any makefile whose name
86
+ # coincides with the name of the project.
87
+ def prepare_derivative_build(project)
88
+ mf = project.makefile
89
+ return false if mf.nil?
90
+ debug "Preparing derivative build for #{mf.basename}"
91
+ submake = Makefile.new(mf)
92
+ subplatform = Platform.new(@platform.local_path)
93
+ subplatform.contrib_path = @platform.dest_path(project)
94
+ new_updater = Updater.new(submake, subplatform, site)
95
+ new_updater.exclude(project.extensions)
96
+ new_updater.exclude(@platform.profiles)
97
+ @derivative_builds << new_updater
98
+ return true
99
+ end
100
+
101
+ # Tries to reconcile the makefile with the platform by resolving unmet
102
+ # dependencies and determining which projects must be installed, upgraded,
103
+ # downgraded, moved or removed. This method does not return anything.
104
+ # The result of the synchronization can be inspected by accessing
105
+ # Drupid::Updater#log.
106
+ #
107
+ # This method does not modify the platform at all, it only preflights changes
108
+ # and caches the needed stuff locally. For changes to be applied,
109
+ # Drupid::Updater#apply_changes must be invoked after this method
110
+ # has been invoked.
111
+ #
112
+ # If :nofollow is set to true, then this method does not try to resolve missing
113
+ # dependencies: it only checks the projects that are explicitly mentioned
114
+ # in the makefile. If :nocore is set to true, only contrib projects are
115
+ # synchronized; otherwise, Drupal core is synchronized, too.
116
+ #
117
+ #
118
+ # See also: Drupid::Updater#apply_changes
119
+ #
120
+ # Options: nofollow, nocore, nolibs
121
+ def sync(options = {})
122
+ @log.clear
123
+ @platform.analyze
124
+ # These paths are needed because Drupal allows libraries to be installed
125
+ # inside modules. Hence, we must ignore them when synchronizing those modules.
126
+ @makefile.each_library do |l|
127
+ @libraries_paths << @platform.local_path + @platform.contrib_path + l.target_path
128
+ end
129
+ # We always need a local copy of Drupal core (either the version specified
130
+ # in the makefile or the latest version), even if we are not going to
131
+ # synchronize the core, in order to extract the list of core projects.
132
+ if get_drupal
133
+ if options[:nocore]
134
+ blah "Skipping core"
135
+ else
136
+ sync_drupal_core
137
+ end
138
+ else
139
+ return
140
+ end
141
+ sync_projects(options)
142
+ sync_libraries unless options[:nolibs]
143
+ # Process derivative builds
144
+ @derivative_builds.each do |updater|
145
+ updater.sync(options.merge(:nocore => true))
146
+ @log.merge(updater.log)
147
+ end
148
+ return
149
+ end
150
+
151
+ def get_drupal
152
+ drupal = @makefile.drupal_project
153
+ unless drupal # Nothing to do
154
+ owarn 'No Drupal project specified.'
155
+ return false
156
+ end
157
+ return false unless _fetch_and_patch(drupal)
158
+ # Extract information about core projects, which must not be synchronized
159
+ temp_platform = Platform.new(drupal.local_path)
160
+ temp_platform.analyze
161
+ @core_projects = temp_platform.core_project_names
162
+ return true
163
+ end
164
+
165
+ # Synchronizes Drupal core.
166
+ # Returns true if the synchronization is successful;
167
+ # returns false otherwise.
168
+ def sync_drupal_core
169
+ if @platform.drupal_project
170
+ _compare_versions @makefile.drupal_project, @platform.drupal_project
171
+ else
172
+ log.action(InstallProjectAction.new(@platform, @makefile.drupal_project))
173
+ end
174
+ return true
175
+ end
176
+
177
+ # Synchronizes projects between the makefile and the platform.
178
+ #
179
+ # Options: nofollow
180
+ def sync_projects(options = {})
181
+ exclude(@core_projects) # Skip core projects
182
+ processed = Array.new(excluded) # List of names of processed projects
183
+ dep_queue = Array.new # Queue of Drupid::Project objects whose dependencies must be checked. This is always a subset of processed.
184
+
185
+ @makefile.each_project do |makefile_project|
186
+ dep_queue << makefile_project if sync_project(makefile_project)
187
+ processed += makefile_project.extensions
188
+ end
189
+
190
+ unless options[:nofollow]
191
+ # Recursively get dependent projects.
192
+ # An invariant is that each project in the dependency queue has been processed
193
+ # and cached locally. Hence, it has a version and its path points to the
194
+ # cached copy.
195
+ while not dep_queue.empty? do
196
+ project = dep_queue.shift
197
+ project.dependencies.each do |dependent_project_name|
198
+ unless processed.include?(dependent_project_name)
199
+ debug "Queue dependency: #{dependent_project_name} <- #{project.extended_name}"
200
+ new_project = Project.new(dependent_project_name, project.core)
201
+ dep_queue << new_project if sync_project(new_project)
202
+ @makefile.add_project(new_project)
203
+ processed += new_project.extensions
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ # Determine projects that should be deleted
210
+ pending_delete = @platform.project_names - processed
211
+ pending_delete.each do |p|
212
+ proj = platform.get_project(p)
213
+ if proj.installed?(site)
214
+ log.error "#{proj.extended_name} cannot be deleted because it is installed"
215
+ end
216
+ log.action(DeleteAction.new(platform, proj))
217
+ end
218
+ end
219
+
220
+ # Performs the necessary synchronization actions for the given project.
221
+ # Returns true if the dependencies of the given project must be synchronized, too;
222
+ # returns false otherwise.
223
+ def sync_project(project)
224
+ return false unless _fetch_and_patch(project)
225
+
226
+ # Does this project contains a makefile? If so, enqueue a derivative build.
227
+ has_makefile = prepare_derivative_build(project)
228
+
229
+ # Ignore libraries that may be installed inside this project
230
+ pp = @platform.local_path + @platform.dest_path(project)
231
+ @libraries_paths.each do |lp|
232
+ if lp.fnmatch?(pp.to_s + '/*')
233
+ project.ignore_path(lp.relative_path_from(pp))
234
+ @log.notice("Ignoring #{project.ignore_paths.last} inside #{project.extended_name}")
235
+ end
236
+ end
237
+
238
+ # Does the project exist in the platform? If so, compare the two.
239
+ if @platform.has_project?(project.name)
240
+ platform_project = @platform.get_project(project.name)
241
+ # Fix project location
242
+ new_path = @platform.dest_path(project)
243
+ if @platform.local_path + new_path != platform_project.local_path
244
+ log.action(MoveAction.new(@platform, platform_project, new_path))
245
+ if (@platform.local_path + new_path).exist?
246
+ log.error("#{new_path} already exists. Use --force to overwrite.")
247
+ end
248
+ end
249
+ # Compare versions and log suitable actions
250
+ _compare_versions project, platform_project
251
+
252
+ # If analyzing the platform does not detect the project (e.g., Fusion),
253
+ # we try to see if the directory exists where it is supposed to be.
254
+ elsif (@platform.local_path + @platform.dest_path(project)).exist?
255
+ begin
256
+ platform_project = PlatformProject.new(@platform, @platform.local_path + @platform.dest_path(project))
257
+ rescue => ex
258
+ log.error("#{platform_project.relative_path} exists, but cannot be analyzed: #{ex}")
259
+ log.action(UpdateProjectAction.new(@platform, project))
260
+ end
261
+ _compare_versions project, platform_project
262
+ else # new project
263
+ log.action(InstallProjectAction.new(@platform, project))
264
+ end
265
+
266
+ return (not has_makefile)
267
+ end
268
+
269
+ # Synchronizes libraries between the makefile and the platform.
270
+ def sync_libraries
271
+ debug 'Syncing libraries'
272
+ processed_paths = []
273
+ @makefile.each_library do |lib|
274
+ sync_library(lib)
275
+ processed_paths << lib.target_path
276
+ end
277
+ # Determine libraries that should be deleted from the 'libraries' folder.
278
+ # The above is a bit of an overstatement, as it is basically impossible
279
+ # to detect a "library" in a reliable way. What we actually do is just
280
+ # deleting "spurious" paths inside the 'libraries' folder.
281
+ # Note also that Drupid is not smart enough to find libraries installed
282
+ # inside modules or themes.
283
+ Pathname.glob(@platform.libraries_path + '**/*').each do |p|
284
+ next unless p.directory?
285
+ q = p.relative_path_from(@platform.local_path + @platform.contrib_path)
286
+ # If q is not a prefix of any processed path, or viceversa, delete it
287
+ if processed_paths.find_all { |pp| pp.fnmatch(q.to_s + '*') or q.fnmatch(pp.to_s + '*') }.empty?
288
+ l = Library.new(p.basename)
289
+ l.local_path = p
290
+ log.action(DeleteAction.new(@platform, l))
291
+ # Do not need to delete subdirectories
292
+ processed_paths << q
293
+ end
294
+ end
295
+ end
296
+
297
+ def sync_library(lib)
298
+ return false unless _fetch_and_patch(lib)
299
+
300
+ platform_lib = Library.new(lib.name)
301
+ relpath = @platform.contrib_path + lib.target_path
302
+ libpath = @platform.local_path + relpath
303
+ platform_lib.local_path = libpath
304
+ if platform_lib.exist?
305
+ begin
306
+ diff = lib.file_level_compare_with platform_lib
307
+ rescue => ex
308
+ odie "Failed to verify the integrity of library #{lib.extended_name}: #{ex}"
309
+ end
310
+ if diff.empty?
311
+ log.notice("[OK] #{lib.extended_name} (#{relpath})")
312
+ else
313
+ log.action(UpdateLibraryAction.new(platform, lib))
314
+ log.notice(diff.join("\n"))
315
+ end
316
+ else
317
+ log.action(InstallLibraryAction.new(platform, lib))
318
+ end
319
+ return true
320
+ end
321
+
322
+ # Applies any pending changes. This is the method that actually
323
+ # modifies the platform. Note that applying changes may be
324
+ # destructive (projects may be upgraded, downgraded, deleted from
325
+ # the platform, moved and/or patched).
326
+ # *Always* *backup* your site before calling this method!
327
+ # If :force is set to true, changes are applied even if there are errors.
328
+ #
329
+ # See also: Drupid::Updater.sync
330
+ #
331
+ # Options: force, no_lockfile
332
+ def apply_changes(options = {})
333
+ raise "No changes can be applied because there are errors." if log.errors? and not options[:force]
334
+ log.apply_pending_actions
335
+ @derivative_builds.each { |updater| updater.apply_changes(options.merge(:no_lockfile => true)) }
336
+ @log.clear
337
+ @derivative_builds.clear
338
+ end
339
+
340
+ private
341
+
342
+ # Returns true if the given component is successfully cached and patched;
343
+ # return false otherwise.
344
+ def _fetch_and_patch component
345
+ begin
346
+ component.fetch
347
+ rescue => ex
348
+ @log.error("#{component.extended_name} could not be fetched: #{ex.message}")
349
+ return false
350
+ end
351
+ if component.has_patches?
352
+ begin
353
+ component.patch
354
+ rescue => ex
355
+ @log.error("#{component.extended_name}: #{ex.message}")
356
+ return false
357
+ end
358
+ end
359
+ return true
360
+ end
361
+
362
+ # Compare project versions and log suitable actions.
363
+ def _compare_versions(makefile_project, platform_project)
364
+ update_action = UpdateProjectAction.new(platform, makefile_project)
365
+ case makefile_project <=> platform_project
366
+ when 0 # up to date
367
+ # Check whether the content of the projects is consistent
368
+ begin
369
+ diff = makefile_project.file_level_compare_with platform_project
370
+ rescue => ex
371
+ odie "Failed to verify the integrity of #{makefile_project.extended_name}: #{ex}"
372
+ end
373
+ p = (makefile_project.drupal?) ? '' : ' (' + (platform.contrib_path + makefile_project.target_path).to_s + ')'
374
+ if diff.empty?
375
+ @log.notice("[OK] #{platform_project.extended_name}#{p}")
376
+ elsif makefile_project.has_patches?
377
+ log.action(update_action)
378
+ log.notice "#{makefile_project.extended_name}#{p} will be patched"
379
+ log.notice(diff.join("\n"))
380
+ else
381
+ log.error("#{platform_project.extended_name}#{p}: mismatch with cached copy:\n" + diff.join("\n"))
382
+ log.action(update_action)
383
+ end
384
+ when 1 # upgrade
385
+ log.action(update_action)
386
+ when -1 # downgrade
387
+ log.action(update_action)
388
+ if platform_project.drupal?
389
+ if @platform.bootstrapped?
390
+ log.error("#{platform_project.extended_name} cannot be downgraded because it is bootstrapped")
391
+ end
392
+ elsif platform_project.installed?(site)
393
+ log.error("#{platform_project.extended_name}#{p} must be uninstalled before downgrading")
394
+ end
395
+ when nil # One or both projects have no version
396
+ # Check whether the content of the projects is consistent
397
+ begin
398
+ diff = makefile_project.file_level_compare_with platform_project
399
+ rescue => ex
400
+ odie "Failed to verify the integrity of #{component.extended_name}: #{ex}"
401
+ end
402
+ if diff.empty?
403
+ log.notice("[OK] #{platform_project.extended_name}#{p}")
404
+ else
405
+ log.action(update_action)
406
+ log.notice(diff.join("\n"))
407
+ if platform_project.has_version? and (not makefile_project.has_version?)
408
+ log.error("Cannot upgrade #{makefile_project.name} from known version to unknown version")
409
+ end
410
+ end
411
+ end
412
+ end
413
+
414
+ public
415
+
416
+ class Log
417
+ include Drupid::Utils
418
+
419
+ attr :actions
420
+ attr :errors
421
+ attr :warnings
422
+ attr :notices
423
+
424
+ # Creates a new log object.
425
+ def initialize
426
+ @actions = Array.new
427
+ @errors = Array.new
428
+ @warnings = Array.new
429
+ @notices = Array.new
430
+ end
431
+
432
+ # Adds an action to the log.
433
+ def action(a)
434
+ @actions << a
435
+ puts a.msg
436
+ end
437
+
438
+ def actions?
439
+ @actions.size > 0
440
+ end
441
+
442
+ def pending_actions?
443
+ @actions.find_all { |a| a.pending? }.size > 0
444
+ end
445
+
446
+ def apply_pending_actions
447
+ @actions.find_all { |a| a.pending? }.each do |pa|
448
+ pa.fire!
449
+ puts pa.msg
450
+ end
451
+ end
452
+
453
+ # Adds an error message to the log.
454
+ def error(msg)
455
+ @errors << msg
456
+ puts @errors.last
457
+ end
458
+
459
+ # Returns true if this log contains error messages;
460
+ # returns false otherwise.
461
+ def errors?
462
+ @errors.size > 0
463
+ end
464
+
465
+ # Adds a warning to the log.
466
+ def warning(msg)
467
+ @warnings << msg
468
+ puts @warnings.last
469
+ end
470
+
471
+ def warnings?
472
+ @warnings.size > 0
473
+ end
474
+
475
+ # Adds a notice to the log.
476
+ def notice(msg)
477
+ @notices << msg
478
+ blah @notices.last
479
+ end
480
+
481
+ def notices?
482
+ @notices.size > 0
483
+ end
484
+
485
+ # Clears the whole log.
486
+ def clear
487
+ @errors.clear
488
+ @warnings.clear
489
+ @notices.clear
490
+ end
491
+
492
+ # Appends the content of another log to this one.
493
+ def merge(other)
494
+ @actions += other.actions
495
+ @errors += other.errors
496
+ @warnings += other.warnings
497
+ @notices += other.notices
498
+ end
499
+
500
+ end # class Log
501
+
502
+
503
+ class AbstractAction
504
+ include Drupid::Utils
505
+ attr :platform
506
+ attr :component
507
+
508
+ def initialize(p, c)
509
+ @platform = p
510
+ @component = c
511
+ @pending = true
512
+ end
513
+
514
+ def fire!
515
+ _install # Implemented by subclasses
516
+ @pending = false
517
+ end
518
+
519
+ def pending?
520
+ @pending
521
+ end
522
+ end # AbstractAction
523
+
524
+
525
+ class UpdateProjectAction < AbstractAction
526
+ def initialize(p, proj)
527
+ raise "#{proj.extended_name} does not exist locally" unless proj.exist?
528
+ raise "Unknown type for #{proj.extended_name}" unless proj.proj_type
529
+ super
530
+ end
531
+
532
+ def msg
533
+ if old_project = platform.get_project(component.name)
534
+ "#{Tty.blue}[Update]#{Tty.white} #{old_project.extended_name} => #{component.extended_name}#{Tty.reset} (#{platform.dest_path(component)})"
535
+ else
536
+ "#{Tty.blue}[Update]#{Tty.white} #{component.extended_name}#{Tty.reset} (#{platform.dest_path(component)})"
537
+ end
538
+ end
539
+
540
+ protected
541
+
542
+ # Deploys a project into the specified location.
543
+ # Note that the content of the
544
+ # project is copied into new_path, not inside a subdirectory of new_path
545
+ # (for example, to copy mymodule inside /some/location, new_path
546
+ # should be set to '/some/location/mymodule').
547
+ # Returns a new Drupid::Project object for the new location, while
548
+ # this project remains unchanged.
549
+ def _install
550
+ args = Array.new
551
+ # If the project contains a makefile, it is a candidate for a derivative build.
552
+ # In such case, protect 'libraries', 'modules' and 'themes' subdirectories
553
+ # from deletion.
554
+ if component.makefile
555
+ args << '-f' << 'P /libraries/***' # this syntax requires rsync >=2.6.7.
556
+ args << '-f' << 'P /modules/***'
557
+ args << '-f' << 'P /profiles/***'
558
+ args << '-f' << 'P /themes/***'
559
+ end
560
+ if component.drupal?
561
+ args = Array.new
562
+ args << '-f' << 'R /profiles/default/***' # D6
563
+ args << '-f' << 'R /profiles/minimal/***' # D7
564
+ args << '-f' << 'R /profiles/standard/***' # D7
565
+ args << '-f' << 'R /profiles/testing/***' # D7
566
+ args << '-f' << 'P /profiles/***'
567
+ args << '-f' << 'R /sites/all/README.txt'
568
+ args << '-f' << 'R /sites/default/default.settings.php'
569
+ args << '-f' << 'P /sites/***'
570
+ end
571
+ args << '-a'
572
+ args << '--delete'
573
+ component.ignore_paths.each { |p| args << "--exclude=#{p}" }
574
+ dst_path = platform.local_path + platform.dest_path(component)
575
+ debug "Pathname.mkpath may raise harmless exceptions"
576
+ dst_path.mkpath unless dst_path.exist?
577
+ args << component.local_path.to_s + '/'
578
+ args << dst_path.to_s + '/'
579
+ begin
580
+ runBabyRun 'rsync', args
581
+ rescue => ex
582
+ odie "Installing or updating #{component.name} failed: #{ex}"
583
+ end
584
+ end
585
+ end # UpdateProjectAction
586
+
587
+
588
+ class InstallProjectAction < UpdateProjectAction
589
+ def initialize(platform, project)
590
+ raise "#{project.name} already exists." if platform.get_project(project.name)
591
+ super
592
+ end
593
+
594
+ def msg
595
+ "#{Tty.blue}[Install]#{Tty.white} #{component.extended_name}#{Tty.reset} (#{platform.dest_path(component)})"
596
+ end
597
+ end # InstallProjectAction
598
+
599
+
600
+ class UpdateLibraryAction < AbstractAction
601
+ def initialize(platform, library)
602
+ raise "#{library.extended_name} does not exist locally" unless library.exist?
603
+ super
604
+ end
605
+
606
+ def msg
607
+ "#{Tty.blue}[Update]#{Tty.white} Library #{component.extended_name}#{Tty.reset} (#{platform.contrib_path + component.target_path})"
608
+ end
609
+
610
+ protected
611
+
612
+ # Deploys a library into the specified location.
613
+ def _install
614
+ args = Array.new
615
+ args << '-a'
616
+ args << '--delete'
617
+ component.ignore_paths.each { |p| args << "--exclude=#{p}" }
618
+ dst_path = platform.local_path + platform.contrib_path + component.target_path
619
+ debug "Pathname.mkpath may raise harmless exceptions"
620
+ dst_path.mkpath unless dst_path.exist?
621
+ args << component.local_path.to_s + '/'
622
+ args << dst_path.to_s + '/'
623
+ begin
624
+ runBabyRun 'rsync', args
625
+ rescue => ex
626
+ odie "Installing or updating library #{component.name} failed: #{ex}"
627
+ end
628
+ end
629
+ end # UpdateLibraryAction
630
+
631
+
632
+ class InstallLibraryAction < UpdateLibraryAction
633
+ def msg
634
+ "#{Tty.blue}[Install]#{Tty.white} Library #{component.extended_name}#{Tty.reset} (#{platform.contrib_path + component.target_path})"
635
+ end
636
+ end
637
+
638
+ class MoveAction < AbstractAction
639
+ # new_path must be relative to platform.local_path.
640
+ def initialize(platform, component, new_path)
641
+ super(platform, component)
642
+ @destination = Pathname.new(new_path)
643
+ end
644
+
645
+ def fire!
646
+ if component.local_path.exist? # may have disappeared in the meantime (e.g., because of an update)
647
+ dst = platform.local_path + @destination
648
+ debug "Moving #{component.local_path} to #{dst}"
649
+ if dst.exist?
650
+ debug "#{dst} already exists, it will be deleted"
651
+ dst.rmtree
652
+ end
653
+ dst.parent.mkpath
654
+ FileUtils.mv component.local_path.to_s, dst.to_s
655
+ else
656
+ blah "Cannot move #{component.local_path.relative_path_from(platform.local_path)}\n" +
657
+ "(It does not exist any longer)"
658
+ end
659
+ @pending = false
660
+ end
661
+
662
+ def msg
663
+ src = component.local_path.relative_path_from(platform.local_path)
664
+ "#{Tty.blue}[Move]#{Tty.reset} From #{src} to #{@destination}"
665
+ end
666
+ end # MoveAction
667
+
668
+
669
+ class DeleteAction < AbstractAction
670
+ def fire!
671
+ component.local_path.rmtree if component.local_path.exist?
672
+ @pending = false
673
+ end
674
+
675
+ def msg
676
+ "#{Tty.yellow}[Delete]#{Tty.white} #{component.extended_name}#{Tty.reset} " +
677
+ "(#{component.local_path.relative_path_from(platform.local_path)})"
678
+ end
679
+ end # DeleteAction
680
+
681
+ end # Updater
682
+
683
+ end # Drupid