drupid 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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