source: main/trunk/gli/src/org/greenstone/gatherer/file/FileManager.java@ 37191

Last change on this file since 37191 was 37191, checked in by kjdon, 17 months ago

added 'new file' into the right click menu in the collection tree. Its handled using replacefile task, but instead of replacing the current file, its using the specified node as the parent, and adding the new file into that folder

  • Property svn:keywords set to Author Date Id Revision
File size: 22.9 KB
Line 
1/**
2 *#########################################################################
3 *
4 * A component of the Gatherer application, part of the Greenstone digital
5 * library suite from the New Zealand Digital Library Project at the
6 * University of Waikato, New Zealand.
7 *
8 * <BR><BR>
9 *
10 * Author: John Thompson, NZDL Project, University of Waikato
11 *
12 * <BR><BR>
13 *
14 * Copyright (C) 2005 New Zealand Digital Library Project
15 *
16 * <BR><BR>
17 *
18 * This program is free software; you can redistribute it and/or modify
19 * it under the terms of the GNU General Public License as published by
20 * the Free Software Foundation; either version 2 of the License, or
21 * (at your option) any later version.
22 *
23 * <BR><BR>
24 *
25 * This program is distributed in the hope that it will be useful,
26 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 * GNU General Public License for more details.
29 *
30 * <BR><BR>
31 *
32 * You should have received a copy of the GNU General Public License
33 * along with this program; if not, write to the Free Software
34 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
35 *########################################################################
36 */
37package org.greenstone.gatherer.file;
38
39import java.io.File;
40import javax.swing.*;
41import org.greenstone.gatherer.Dictionary;
42import org.greenstone.gatherer.Gatherer;
43import org.greenstone.gatherer.collection.CollectionManager;
44import org.greenstone.gatherer.collection.CollectionTree;
45import org.greenstone.gatherer.collection.CollectionTreeNode;
46import org.greenstone.gatherer.gui.ExplodeMetadataDatabasePrompt;
47import org.greenstone.gatherer.gui.ReplaceSrcDocWithHtmlPrompt;
48import org.greenstone.gatherer.gui.GProgressBar;
49import org.greenstone.gatherer.gui.NewFolderOrFilePrompt;
50import org.greenstone.gatherer.gui.RenamePrompt;
51import org.greenstone.gatherer.gui.tree.DragTree;
52import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
53import org.greenstone.gatherer.util.DragComponent;
54import org.greenstone.gatherer.util.Utility;
55import org.greenstone.gatherer.DebugStream;
56
57/** Manages the moving of files within a separate thread.
58 * @author John Thompson, NZDL Project, University of Waikato
59 */
60public class FileManager
61{
62 /** Not only the queue of files to be moved, but also the object that moves them. */
63 static private FileQueue file_queue = null;
64
65 public static final int COPY = 0;
66 public static final int MOVE = 1;
67
68 public static final int FILE_TYPE = 0;
69 public static final int FOLDER_TYPE = 1;
70
71 protected static File startup_directory = null;
72
73
74 /** Constructor.
75 * @see org.greenstone.gatherer.file.FileQueue
76 */
77 public FileManager()
78 {
79 file_queue = new FileQueue();
80 file_queue.start();
81 }
82
83
84 /** Determine what action should be carried out by the file queue, and add all of the necessary file jobs. */
85 public void action(DragComponent source, FileNode[] source_nodes, DragComponent target, FileNode target_node)
86 {
87 // Check there is something to do
88 if (source_nodes == null || source_nodes.length == 0) {
89 return;
90 }
91
92 // We need a unique ID for each file task
93 long id = System.currentTimeMillis();
94
95 // If source and target are the same we're moving
96 if (source == target) {
97 // Start a new move FileTask and we're done
98 (new FileTask(id, source, source_nodes, target, target_node, FileJob.MOVE)).start();
99 return;
100 }
101
102 // If target isn't the RecycleBin, we're copying
103 if (!(target instanceof RecycleBin)) {
104 // Start a new copy FileTask and we're done
105 (new FileTask(id, source, source_nodes, target, target_node, FileJob.COPY)).start();
106 return;
107 }
108
109 // We're deleting... but first make sure source isn't read-only
110 boolean read_only_source = false;
111
112 // The workspace tree is read-only...
113 if (source.toString().equals("Workspace")) {
114 read_only_source = true;
115
116 // ...except for files from the "Downloaded Files" folder
117 String downloaded_files_folder_path = Gatherer.getGLIUserCacheDirectoryPath();
118 for (int i = 0; i < source_nodes.length; i++) {
119 // Is this the "Downloaded Files" folder?
120 if (source_nodes[i].getFile().getAbsolutePath().startsWith(downloaded_files_folder_path)) {
121 read_only_source = false;
122 }
123 }
124 }
125
126 // The source is read-only, so tell the user and abort
127 if (read_only_source) {
128 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Read_Only"), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
129 return;
130 }
131
132 // Start a new delete FileTask and we're done
133 (new FileTask(id, source, source_nodes, target, target_node, FileJob.DELETE)).start();
134 }
135
136 /** For moving and copying of folders. */
137 public void action(File sourceFolder, File targetFolder, int operation) {
138 (new SimpleFileTask(sourceFolder, targetFolder, operation)).start();
139 }
140
141
142 /** Retrieves the file queue object. */
143 public FileQueue getQueue()
144 {
145 return file_queue;
146 }
147
148 /** Performs the simple file task of moving or copying folders. */
149 private class SimpleFileTask
150 extends Thread
151 {
152 private File sourceFolder;
153 private File targetFolder;
154 int operation; // MOVE or COPY
155
156 public SimpleFileTask(File sourceFolder, File targetFolder, int operation)
157 {
158 this.sourceFolder = sourceFolder;
159 this.targetFolder = targetFolder;
160 this.operation = operation;
161 }
162
163
164 public void run()
165 {
166 // check if we're moving or overwriting the current collection
167 String currentColPath = Gatherer.getCollectDirectoryPath()+CollectionManager.getLoadedCollectionName();
168 if(currentColPath.equals(sourceFolder.getAbsolutePath())
169 || currentColPath.equals(targetFolder.getAbsolutePath())) {
170 Gatherer.g_man.saveThenCloseCurrentCollection();
171 }
172
173 // if moving, try a simple move operation (if it works, it
174 // shouldn't take long at all and doesn't need a progress bar)
175 if(operation == MOVE && sourceFolder.renameTo(targetFolder)) {
176 //System.err.println("**** A simple renameTo() worked.");
177 WorkspaceTreeModel.refreshGreenstoneCollectionsNode();
178 return;
179 }
180
181 // Reset the progress bar and set it to indeterminate while calculating its size
182 GProgressBar progress_bar = file_queue.getProgressBar();
183 progress_bar.reset();
184 progress_bar.setIndeterminate(true);
185
186 String status = "FileActions.Moving";
187 if(operation == COPY) {
188 status = "FileActions.Copying";
189 }
190 progress_bar.setString(Dictionary.get(status));
191 file_queue.getFileStatus().setText(Dictionary.get(status,
192 file_queue.formatPath(status,
193 sourceFolder.getAbsolutePath(),
194 file_queue.getFileStatus().getSize().width)));
195
196 // do the move or copy operation
197 try {
198 //System.err.println("**** Copying " + sourceFolder + " to: " + targetFolder);
199 file_queue.copyDirectoryContents(sourceFolder, targetFolder);
200 } catch(Exception e) {
201 JOptionPane.showMessageDialog(Gatherer.g_man, e.getMessage(),
202 "Can't perform file operation", JOptionPane.ERROR_MESSAGE);
203
204 progress_bar.setIndeterminate(false);
205 progress_bar.clear();
206 return;
207 }
208
209 // if moving, delete the original source folder and
210 // update the docs in GS collections node in the workspace tree
211
212 if(operation == MOVE) {
213 Utility.delete(sourceFolder);
214 WorkspaceTreeModel.refreshGreenstoneCollectionsNode();
215 }
216
217
218 progress_bar.setIndeterminate(false);
219 progress_bar.clear();
220 file_queue.getFileStatus().setText(Dictionary.get("FileActions.No_Activity"));
221 progress_bar.setString(Dictionary.get("FileActions.No_Activity"));
222 }
223 }
224
225 private class FileTask
226 extends Thread
227 {
228 private long id;
229 private DragComponent source;
230 private FileNode[] source_nodes;
231 private DragComponent target;
232 private FileNode target_node;
233 private byte type;
234
235
236 public FileTask(long id, DragComponent source, FileNode[] source_nodes, DragComponent target, FileNode target_node, byte type)
237 {
238 this.id = id;
239 this.source = source;
240 this.source_nodes = source_nodes;
241 this.target = target;
242 this.target_node = target_node;
243 this.type = type;
244 }
245
246
247 public void run()
248 {
249 // Reset the progress bar and set it to indeterminate while calculating its size
250 GProgressBar progress_bar = file_queue.getProgressBar();
251 progress_bar.reset();
252 progress_bar.setIndeterminate(true);
253
254 // Calculate the progress bar size
255 boolean cancelled = file_queue.calculateSize(source_nodes);
256 if (!cancelled) {
257 file_queue.addJob(id, source, source_nodes, target, target_node, type);
258 if (Gatherer.isGsdlRemote) {
259 String collection_name = CollectionManager.getLoadedCollectionName();
260
261 // Perform the appropriate action based on the job type (RemoteGreenstoneServer will queue)
262 if (type == FileJob.COPY) {
263 // Copies: upload all the files at once in one zip file
264 File[] source_files = new File[source_nodes.length];
265 for (int i = 0; i < source_nodes.length; i++) {
266 source_files[i] = source_nodes[i].getFile();
267 }
268 Gatherer.remoteGreenstoneServer.uploadFilesIntoCollection(collection_name, source_files, target_node.getFile());
269 }
270 else if (type == FileJob.DELETE) {
271 // Deletes: delete each top-level file/directory one at a time
272 for (int i = 0; i < source_nodes.length; i++) {
273 Gatherer.remoteGreenstoneServer.deleteCollectionFile(collection_name, source_nodes[i].getFile());
274 }
275 }
276 else if (type == FileJob.MOVE) {
277 // Moves: move each top-level file/directory one at a time
278 for (int i = 0; i < source_nodes.length; i++) {
279 Gatherer.remoteGreenstoneServer.moveCollectionFile(
280 collection_name, source_nodes[i].getFile(), target_node.getFile());
281 }
282 }
283 }
284 }
285
286 progress_bar.setIndeterminate(false);
287 progress_bar.clear();
288 }
289 }
290
291 public void explodeMetadataDatabase(File file)
292 {
293 // This must go in a separate thread because we need the progress bar to work (remote Greenstone server)
294 new ExplodeMetadataDatabasePromptTask(file).start();
295 }
296
297 // Works with replace_srcdoc_with_html.pl
298 public void replaceSrcDocWithHtml(File[] files)
299 {
300 // This must go in a separate thread because we need the progress bar to work (remote Greenstone server)
301 new ReplaceSrcDocWithHtmlPromptTask(files).start();
302 }
303
304 private class ExplodeMetadataDatabasePromptTask
305 extends Thread
306 {
307 private File metadata_database_file = null;
308
309 public ExplodeMetadataDatabasePromptTask(File metadata_database_file)
310 {
311 this.metadata_database_file = metadata_database_file;
312 }
313
314 public void run()
315 {
316 ExplodeMetadataDatabasePrompt emp = new ExplodeMetadataDatabasePrompt(metadata_database_file);
317 }
318 }
319
320 // Works with replace_srcdoc_with_html.pl
321 private class ReplaceSrcDocWithHtmlPromptTask
322 extends Thread
323 {
324 private File[] replace_these_srcdoc_files = null;
325
326 public ReplaceSrcDocWithHtmlPromptTask(File[] replace_these_srcdoc_files)
327 {
328 this.replace_these_srcdoc_files = replace_these_srcdoc_files;
329 }
330
331 public void run()
332 {
333 ReplaceSrcDocWithHtmlPrompt prompt = new ReplaceSrcDocWithHtmlPrompt(replace_these_srcdoc_files);
334 }
335 }
336
337
338 public void openFileInExternalApplication(File file)
339 {
340 // This must go in a separate thread because we need the progress bar to work (remote Greenstone server)
341 new OpenFileInExternalApplicationTask(file).start();
342 }
343
344
345 private class OpenFileInExternalApplicationTask
346 extends Thread
347 {
348 private File file = null;
349
350 public OpenFileInExternalApplicationTask(File file)
351 {
352 this.file = file;
353 }
354
355 public void run()
356 {
357 // If we're using a remote Greenstone server, we need to download the file before viewing it...
358 if (Gatherer.isGsdlRemote) {
359 // ... but only if it is inside the collection and we haven't already downloaded it
360 if (file.getAbsolutePath().startsWith(Gatherer.getCollectDirectoryPath()) && file.length() == 0) {
361 if (Gatherer.remoteGreenstoneServer.downloadCollectionFile(
362 CollectionManager.getLoadedCollectionName(), file).equals("")) {
363 // Something has gone wrong downloading the file
364 return;
365 }
366 }
367 }
368
369 // View the file in an external application
370 Gatherer.spawnApplication(file);
371 }
372 }
373
374
375 public void newDummyDoc(DragTree tree, CollectionTreeNode parent_node){
376 newFolderOrDummyDoc(tree, parent_node, FILE_TYPE);
377 }
378
379
380 public void newFolder(DragTree tree, CollectionTreeNode parent_node) {
381 newFolderOrDummyDoc(tree, parent_node, FOLDER_TYPE);
382 }
383
384
385 protected void newFolderOrDummyDoc(DragTree tree, CollectionTreeNode parent_node, int type) {
386 (new NewFolderOrDummyDocumentTask(tree, parent_node, type)).start();
387 }
388
389
390 private class NewFolderOrDummyDocumentTask
391 extends Thread
392 {
393 private DragTree tree = null;
394 private CollectionTreeNode parent_node = null;
395 private int type;
396
397 public NewFolderOrDummyDocumentTask(DragTree tree, CollectionTreeNode parent_node, int type)
398 {
399 this.tree = tree;
400 this.parent_node = parent_node;
401 this.type = type;
402 }
403
404 public void run()
405 {
406 // Ask the user for the directories name.
407 String extension = "";
408 if (type == FILE_TYPE) {
409 extension = ".nul";
410 }
411
412 NewFolderOrFilePrompt new_folder_prompt = new NewFolderOrFilePrompt(parent_node, type, extension);
413 String name = new_folder_prompt.display();
414 new_folder_prompt.dispose();
415 new_folder_prompt = null;
416
417 // And if the name is non-null...
418 if (name != null) {
419 FileSystemModel model = (FileSystemModel) tree.getModel();
420 File folder_file = new File(parent_node.getFile(), name);
421
422 //... check if it already exists.
423 if (folder_file.exists()) {
424 if (type == FILE_TYPE) {
425 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.File_Already_Exists_No_Create", name), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
426 }
427 else {
428 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Folder_Already_Exists", name), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
429 }
430 }
431 // Otherwise create it.
432 else {
433 try {
434 if (type == FILE_TYPE) {
435 folder_file.createNewFile();
436 if (Gatherer.isGsdlRemote) {
437 Gatherer.remoteGreenstoneServer.uploadCollectionFile(
438 CollectionManager.getLoadedCollectionName(), folder_file);
439 }
440 }
441 else {
442 folder_file.mkdirs();
443 if (Gatherer.isGsdlRemote) {
444 Gatherer.remoteGreenstoneServer.newCollectionDirectory(
445 CollectionManager.getLoadedCollectionName(), folder_file);
446 }
447 }
448
449 // Update the parent node to show the new folder
450 parent_node.refresh();
451
452 // Refresh workspace tree (collection tree is done automatically)
453 Gatherer.g_man.refreshWorkspaceTree(DragTree.COLLECTION_CONTENTS_CHANGED);
454 }
455 catch (Exception exception) {
456 if (type == FILE_TYPE) {
457 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.File_Create_Error", name), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
458 }
459 else {
460 JOptionPane.showMessageDialog(Gatherer.g_man, Dictionary.get("FileActions.Folder_Create_Error", name), Dictionary.get("General.Error"), JOptionPane.ERROR_MESSAGE);
461 }
462 }
463 }
464
465 folder_file = null;
466 model = null;
467 }
468 name = null;
469 }
470 }
471
472
473 public void renameCollectionFile(CollectionTree collection_tree, CollectionTreeNode collection_tree_node)
474 {
475 // This must go in a separate thread because we need the progress bar to work (remote Greenstone server)
476 new RenameTask(collection_tree, collection_tree_node).start();
477 }
478
479
480 private class RenameTask
481 extends Thread
482 {
483 private CollectionTree collection_tree = null;
484 private CollectionTreeNode collection_tree_node = null;
485
486 public RenameTask(CollectionTree collection_tree, CollectionTreeNode collection_tree_node)
487 {
488 this.collection_tree = collection_tree;
489 this.collection_tree_node = collection_tree_node;
490 }
491
492 public void run()
493 {
494 RenamePrompt rename_prompt = new RenamePrompt(collection_tree_node);
495 String new_collection_file_name = rename_prompt.display();
496 rename_prompt.dispose();
497 rename_prompt = null;
498
499 if (new_collection_file_name != null) {
500 File collection_file = collection_tree_node.getFile();
501 File new_collection_file = new File(collection_file.getParentFile(), new_collection_file_name);
502 CollectionTreeNode new_collection_tree_node = new CollectionTreeNode(new_collection_file);
503 file_queue.addJob(System.currentTimeMillis(), collection_tree, new FileNode[] { collection_tree_node }, collection_tree, new_collection_tree_node, FileJob.RENAME);
504 if (Gatherer.isGsdlRemote) {
505 Gatherer.remoteGreenstoneServer.moveCollectionFile(
506 CollectionManager.getLoadedCollectionName(), collection_file, new_collection_file);
507 }
508 }
509 }
510 }
511
512 public void newCollectionFile(CollectionTree collection_tree, CollectionTreeNode parent_node)
513 {
514 new ReplaceOrNewTask(collection_tree, parent_node, false ).start();
515 }
516
517 public void replaceCollectionFile(CollectionTree collection_tree, CollectionTreeNode collection_tree_node)
518 {
519 // This must go in a separate thread because we need the progress bar to work (remote Greenstone server)
520 new ReplaceOrNewTask(collection_tree, collection_tree_node, true).start();
521 }
522
523
524 private class ReplaceOrNewTask
525 extends Thread implements FileCopiedSuccessListener
526 {
527 private CollectionTree collection_tree = null;
528 private CollectionTreeNode collection_tree_node = null;
529 private boolean replacing = false;
530
531 public ReplaceOrNewTask(CollectionTree collection_tree, CollectionTreeNode collection_tree_node, boolean replacing)
532 {
533 this.collection_tree = collection_tree;
534 this.collection_tree_node = collection_tree_node; // either the file to replace, or the folder to go into
535 this.replacing = replacing;
536 }
537
538 public void run()
539 {
540 JFileChooser file_chooser = new JFileChooser(startup_directory);
541 if (replacing) {
542 file_chooser.setDialogTitle(Dictionary.get("ReplacePrompt.Title"));
543 } else {
544 file_chooser.setDialogTitle(Dictionary.get("NewPrompt.Title"));
545 }
546 File new_file = null;
547 int return_val = file_chooser.showOpenDialog(null);
548 if(return_val == JFileChooser.APPROVE_OPTION) {
549 new_file = file_chooser.getSelectedFile();
550 }
551
552 if (new_file == null) {
553 return;
554 }
555
556 // save the search path for next time
557 startup_directory = new_file.getParentFile();
558 // make up a node for the file to bring in
559 WorkspaceTreeNode source_node = new WorkspaceTreeNode(new_file);
560
561 //DebugStream.setDebugging(true, "FileManager.ReplaceTask");
562
563 // Some different handling if the old and new tail file names are the same and target file goes into the same
564 // location in collection tree as source.
565 // This avoids past errors upon replacing with same filename (diff file contents) where the attached meta gets lost,
566 // or remote file gets updated but file gone missing in client-GLI view until collection reopened.
567 boolean isSameLeafName = false;
568 if(replacing && collection_tree_node.getFile().getName().equals(new_file.getName())) {
569 DebugStream.println(" @@@ File Replace: New file has the same name as existing.");
570 isSameLeafName = true;
571 }
572
573 File target_directory;
574 FileNode parent; // store the original source's parent, need it several times after changing source
575 if (replacing) {
576 target_directory = collection_tree_node.getFile().getParentFile();
577 parent = (FileNode)collection_tree_node.getParent();
578 } else {
579 target_directory = collection_tree_node.getFile();
580 parent = collection_tree_node;
581 }
582 CollectionTreeNode new_collection_tree_node = new CollectionTreeNode(new File(target_directory, new_file.getName()));
583
584 //FileNode parent = (FileNode)collection_tree_node.getParent(); // store the original source's parent, need it several times after changing source
585
586 if(isSameLeafName) {
587 // If the file name of the replacing file IS the same as the one being replaced
588 // perform a COPY operation, which will copy across metadata too, after confirming whether the user really wants to replace the source with identically named target
589
590 // (a) First, this instance of ReplaceTask and no other starts listening to whether the user
591 // DIDN'T CANCEL out of an identical filename copy operation and if this local file copy
592 // was a success. If so, on successful file copy event fired (only then), the source file
593 // from the workspace tree will also be uploaded to the remote GS3
594 file_queue.addFileCopiedSuccessListener(this);
595
596 // (b) Now can finally add the COPY job to the queue
597 file_queue.addJob(System.currentTimeMillis(), Gatherer.g_man.gather_pane.workspace_tree, new FileNode[] { source_node }, collection_tree, parent, FileJob.COPY);
598
599 } else {
600 // If the file name of the replacing file is NOT the same as the one being replaced:
601 // (a) Again, this ReplaceTask instance needs to listen for the file copy event fired,
602 // so that the source file will also get uploaded to the remote GS3 on FileCopiedSuccess
603 file_queue.addFileCopiedSuccessListener(this);
604
605 // (b) copy the new file in - but don't bring metadata
606 file_queue.addJob(System.currentTimeMillis(), Gatherer.g_man.gather_pane.workspace_tree, new FileNode[] { source_node }, collection_tree, parent, FileJob.COPY_FILE_ONLY);
607
608 if (replacing) {
609 // (c) final step to finish off: do a replace of old file with new file
610 file_queue.addJob(System.currentTimeMillis(), collection_tree, new FileNode[] { collection_tree_node }, collection_tree, new_collection_tree_node, FileJob.REPLACE);
611 }
612 }
613
614
615
616 //DebugStream.setDebugging(false, "FileManager.ReplaceTask");
617 }
618
619
620 /** In order to detect that the user cancelled out of replacing an identically named target file,
621 * we now listen to events fired that the file was successfully copied across. Only then do we
622 * bother transferring the source file (from the workspace) into the target location in the
623 * collection on the remote file system. We don't do this if the user cancelled.
624 */
625 public void fileCopiedSuccessfully(File new_file) {
626
627 //DebugStream.setDebugging(true, "FileManager.ReplaceTask.fileCopiedSuccessfully");
628
629 if (Gatherer.isGsdlRemote) {
630 File target_directory;
631 if (this.replacing) {
632 target_directory = this.collection_tree_node.getFile().getParentFile();
633 } else {
634 target_directory = this.collection_tree_node.getFile();
635 }
636 File collection_tree_node_file = this.collection_tree_node.getFile();
637
638 String collection_name = CollectionManager.getLoadedCollectionName();
639 if (this.replacing) {
640 Gatherer.remoteGreenstoneServer.deleteCollectionFile(collection_name, collection_tree_node_file);
641 }
642 Gatherer.remoteGreenstoneServer.uploadFilesIntoCollection(collection_name, new File[] { new_file }, target_directory);
643 }
644
645 // stop listening to further events fired now that we've handled this event successfully
646 file_queue.removeFileCopiedSuccessListener(this);
647 //DebugStream.setDebugging(false, "FileManager.ReplaceTask.fileCopiedSuccessfully");
648 }
649 }
650}
Note: See TracBrowser for help on using the repository browser.