/** *######################################################################### * * A component of the Gatherer application, part of the Greenstone digital * library suite from the New Zealand Digital Library Project at the * University of Waikato, New Zealand. * * Author: John Thompson, Greenstone Digital Library, University of Waikato * * Copyright (C) 1999 New Zealand Digital Library Project * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. *######################################################################## */ package com.gs3.testGXT.server.Greenstone; import java.io.*; import java.util.*; import org.greenstone.gatherer.DebugStream; import org.greenstone.gatherer.Gatherer; import org.greenstone.gatherer.file.FileAlreadyExistsException; import org.greenstone.gatherer.file.InsufficientSpaceException; import org.greenstone.gatherer.file.ReadNotPermittedException; import org.greenstone.gatherer.file.UnknownFileErrorException; import org.greenstone.gatherer.file.WriteNotPermittedException; import org.greenstone.gatherer.gui.tree.DragTree; import org.greenstone.gatherer.util.StaticStrings; import org.greenstone.gatherer.util.Utility; /** A threaded object which processes a queue of file actions such as copying and movement. It also handles updating the various trees involved so they are an accurate representation of the file system they are meant to match. * @author John Thompson, Greenstone Digital Library, University of Waikato * @version 2.3 */ public class GWTFileQueue extends Thread { /** The size of the io buffer, in bytes. */ static final private int BUFFER_SIZE = 1024; public static int job_size = 0; public static int done_size = 0; /** A list containing a queue of waiting movement jobs. */ private ArrayList queue = null; private static int allowance = 0; public static int files_in_queue = 1; public static Object monitor = new Object(); /** * Set copy allowance of filequeue * @param _allowance allowance in 1024 byte chunks * Idealy, this would be windowed based on something like sliding window, idk */ public static void cancelOperations() { wasCancelled = cancelled = true; } private static boolean isQueueBusy = false; public static void queue_busy() {isQueueBusy = true;} private static void queue_free() {isQueueBusy = false;} public static boolean isQueueBusy() {return isQueueBusy;} private static boolean cancelled = false; private static boolean wasCancelled = false; public static boolean wasCancelled() { boolean tmp = wasCancelled; wasCancelled = false; return tmp; } public void checkQueue() { files_in_queue = 1; } public static void setAllowance(int _allowance) { allowance = _allowance; } public static int getAllowance() { return allowance; } public static String currentJob = null; /** Constructor. */ public GWTFileQueue() { DebugStream.println("FileQueue started."); this.queue = new ArrayList(); } /** Add a new job to the queue, specifiying as many arguments as is necessary to complete this type of job (ie delete needs no target information). * @param id A long id unique to all jobs created by a single action. * @param source The DragComponent source of this file, most likely a DragTree. * @param child The FileNode you wish to mode. * @param target The DragComponent to move the file to, again most likely a DragTree. * @param parent The files new FileNode parent within the target. * @param type The type of this movement as an int, either COPY or DELETE. */ public void addJob(long id, GWTFileNode source, GWTFileNode[] children, GWTFileNode target, byte type) { //TODO: we may have to modify children, target to fit parameters properly // Queue the sub-job(s) (this may fail if we are asked to delete a read only file) for (int i = 0; i < children.length; i++) { addJob(id, source, children[i], target, type, -1); } files_in_queue = 1; } synchronized public void addJob(long id, GWTFileNode source, GWTFileNode target, byte type) { GWTFileJob job = new GWTFileJob(id, source, target, type); source.refreshChildren(); DebugStream.println("Adding job: " + job); queue.add(job); if(type == GWTFileJob.COPY_FILE_ONLY) job_size += source.file.length(); files_in_queue = 1; notify(); } synchronized private void addJob(long id, GWTFileNode source, GWTFileNode child, GWTFileNode target,byte type, int position) { GWTFileJob job = new GWTFileJob(id, source, target, type); if(type == GWTFileJob.COPY) job_size += source.file.length(); DebugStream.println("Adding job: " + job); if(position != -1 && position <= queue.size() + 1) { queue.add(position, job); } else { queue.add(job); } files_in_queue = 1; notify(); } /** Calculates the total deep file size of the selected file nodes. * @param files a FileNode[] of selected files * @return true if a cancel was signaled, false otherwise * @see org.greenstone.gatherer.file.FileManager.Task#run() */ public boolean calculateSize(ArrayList files) { //job_size = 0; // Calculate the total file size of all the selected file nodes Vector remaining = new Vector(); for (int i = 0; i < files.size(); i++) { remaining.add(files.get(i)); } while (remaining.size() > 0) { GWTFileNode node = remaining.remove(0); job_size += node.getFile().length(); if (node.isLeaf()) { job_size += node.getFile().length(); } else { for (int i = 0; i < node.getChildCount(); i++) { remaining.add((GWTFileNode) node.getChildAt(i)); } } } // Now we return if calculation was cancelled so that the FileManagers Task can skip the addJob phase correctly. /*if (cancel_action) { cancel_action = false; // reset return true; } else { */ return false; //} } /** This method is called to cancel the job queue at the next available moment. */ public void cancelAction() { clearJobs(); files_in_queue = queue.size(); } //this doesn't seem to ever even happen synchronized private void addFileJob(long id, GWTFileNode source, GWTFileNode target, byte type) { queue.add(new GWTFileJob(id, source, target, type)); //job_size += source.file.length(); //System.err.println(source.file.length()); files_in_queue = 1; notify(); } private void doEmptyDirectoryDelete(GWTFileJob file_job) { GWTFileNode source_node = file_job.getOrigin(); File source_directory = source_node.getFile(); // If the directory isn't empty then this will fail if (source_directory.delete() == false) { // The source directory couldn't be deleted, so give the user the option of continuing or cancelling //TODO: there should be a dialog here System.err.println("Could not delete item - non-empty directory"); clearJobs(); // Aborting action return; } } private void doDirectoryDelete(GWTFileJob file_job) { GWTFileNode source_node = file_job.getOrigin(); File source_directory = source_node.getFile(); // Add a new Delete job for each child of this directory (except metadata.xml files) source_node.refresh(); for (int i = 0; i < source_node.size(); i++) { GWTFileNode child_file_node = (GWTFileNode) source_node.getChildAtUnfiltered(i); if (!child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) { addFileJob(file_job.ID(), child_file_node, null, GWTFileJob.DELETE); } } // Treat metadata.xml files spehttp://www.waikato.ac.nz/contacts/map/cially: delete them first for (int i = 0; i < source_node.size(); i++) { GWTFileNode child_file_node = (GWTFileNode) source_node.getChildAtUnfiltered(i); if (child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) { addFileJob(file_job.ID(), child_file_node, null, GWTFileJob.DELETE); break; } } // The last thing we will do is delete this directory (which should be empty by then) addFileJob(file_job.ID(), source_node, null, GWTFileJob.DELETE_EMPTY_DIRECTORY); } private void doDirectoryCopy(GWTFileJob file_job) { GWTFileNode source_node = file_job.getOrigin(); GWTFileNode target_node = file_job.getDestination(); File source_directory = source_node.getFile(); File target_directory = new File(target_node.getFile(), source_directory.getName()); // Check that the source directory doesn't contain the target directory (will create a cyclic loop) if (target_directory.getAbsolutePath().startsWith(source_directory.getAbsolutePath())) { System.err.println("Item contains itself: cannot copy"); clearJobs(); // Aborting action return; } // The target directory shouldn't already exist if (target_directory.exists()) { System.err.println("Target already exists..."); clearJobs(); // Aborting action return; } target_directory.mkdirs(); // Create a node for the new directory in the collection tree /*FileSystemModel target_model = new FileSystemModel((FileSystemModel) file_job.target.getTreeModel(); CollectionTreeNode new_target_node = new CollectionTreeNode(target_directory); SynchronizedTreeModelTools.insertNodeInto(target_model, target_node, new_target_node); new_target_node.setParent(target_node); // Copy the non-folder level metadata assigned to the original directory to the new directory ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToExternalFile(source_directory); MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata); */ // Add a new Copy job for each child of this directory (except metadata.xml files) //TODO: will need to do a lot of hacking here to make it work /* */ String target = target_node.getFile().getAbsolutePath() + "/" + source_node.getFile().getName(); System.err.println("Target: " + target); GWTFileNode new_target = new GWTFileNode(new File(target)); source_node.refreshChildren(); for (int i = 0; i < source_node.size(); i++) { GWTFileNode child_file_node = (GWTFileNode) source_node.getChildAtUnfiltered(i); if (!child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) { addFileJob(file_job.ID(), child_file_node, new_target, GWTFileJob.COPY); } } } private void doDirectoryMove(GWTFileJob file_job) { GWTFileNode source_node = file_job.getOrigin(); GWTFileNode target_node = file_job.getDestination(); File source_directory = source_node.getFile(); File target_directory = new File(target_node.getFile(), source_directory.getName()); if (file_job.type == GWTFileJob.RENAME) { // This is the only difference between moves and renames target_directory = target_node.getFile(); target_node = (GWTFileNode) source_node.getParent(); } // Check the target directory isn't the source directory if (target_directory.equals(source_directory)) { DebugStream.println("Target directory is the source directory!"); return; } // The target directory shouldn't already exist if (target_directory.exists()) { System.err.println("Target directory already exists"); //todo: error dialog clearJobs(); // Aborting action return; } target_directory.mkdirs(); /* // Create a node for the new directory in the collection tree FileSystemModel target_model = (FileSystemModel) file_job.target.getTreeModel(); CollectionTreeNode new_target_node = new CollectionTreeNode(target_directory); SynchronizedTreeModelTools.insertNodeInto(target_model, target_node, new_target_node); new_target_node.setParent(target_node); // Move the folder level metadata assigned to the original directory to the new directory ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_directory); MetadataXMLFileManager.removeMetadata((CollectionTreeNode) source_node, assigned_metadata); MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata); // The last thing we will do is delete this directory addFileJob(file_job.ID(), file_job.source, source_node, null, null, FileJob.DELETE); // Treat metadata.xml files specially: delete them last source_node.refresh(); for (int i = 0; i < source_node.size(); i++) { FileNode child_file_node = (FileNode) source_node.getChildAtUnfiltered(i); if (child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) { addFileJob(file_job.ID(), file_job.source, child_file_node, null, null, FileJob.DELETE); break; } } // Add a new Move job for each child of this directory (except metadata.xml files) for (int i = 0; i < source_node.size(); i++) { FileNode child_file_node = (FileNode) source_node.getChildAtUnfiltered(i); if (!child_file_node.getFile().getName().equals(StaticStrings.METADATA_XML)) { addFileJob(file_job.ID(), file_job.source, child_file_node, file_job.target, new_target_node, FileJob.MOVE); } } */ } private void doFileDelete(GWTFileJob file_job) { GWTFileNode source_node = file_job.getOrigin(); File source_file = source_node.getFile(); // Almost all files will be deleted from the collection tree (exception: files in "Downloaded Files") /*if (source_node instanceof CollectionTreeNode) { // If we're deleting a metadata.xml file we must unload it boolean metadata_xml_file = source_file.getName().equals(StaticStrings.METADATA_XML); if (metadata_xml_file) { MetadataXMLFileManager.unloadMetadataXMLFile(source_file); } // Otherwise remove any metadata assigned directly to the file else { ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_file); MetadataXMLFileManager.removeMetadata((CollectionTreeNode) source_node, assigned_metadata); } }*/ // Delete the source file if (!Utility.delete(source_file)) { // The source file couldn't be deleted, so give the user the option of continuing or cancelling System.err.println("Couldn't delete file"); //TODO: we'll have to put a dialog or something here later clearJobs(); // Aborting action return; } // Remove the node from the model //SynchronizedTreeModelTools.removeNodeFromParent(file_job.source.getTreeModel(), source_node); } private void doFileCopy(GWTFileJob file_job) { GWTFileNode source_node = file_job.getOrigin(); GWTFileNode target_node = file_job.getDestination(); currentJob = "Copying " + source_node.getURLEncodedFileName() + " to " + target_node.getURLEncodedFilePath(); File source_file = source_node.getFile(); File target_file = new File(target_node.getFile(), source_file.getName()); // The target file shouldn't already exist -- if it does ask the user whether they want to overwrite boolean overwrite_file = false; if (target_file.exists()) { //we always overwrite the file because this has been pre-resolved overwrite_file = true; } // Copy the file try { copyFile(source_file, target_file, true); } catch (FileAlreadyExistsException exception) { // This should not ever happen, since we've called copyFile with overwrite set DebugStream.printStackTrace(exception); return; } catch (FileNotFoundException exception) { DebugStream.printStackTrace(exception); System.err.println("file not found"); //TODO: dialog clearJobs(); // Aborting action // Refresh the source tree model //FileSystemModel source_model = file_job.source.getTreeModel(); //source_model.refresh(new TreePath(((FileNode) file_job.getOrigin().getParent()).getPath())); return; } catch (InsufficientSpaceException exception) { DebugStream.printStackTrace(exception); System.err.println("No space exception"); //TODO: dialog clearJobs(); // Aborting action return; } catch (IOException exception) { DebugStream.printStackTrace(exception); System.err.println("IOexception?"); //TODO: dialog clearJobs(); // Aborting action return; } catch (ReadNotPermittedException exception) { DebugStream.printStackTrace(exception); System.err.println("Read not permitted"); //TODO: dialog clearJobs(); // Aborting action return; } catch (UnknownFileErrorException exception) { DebugStream.printStackTrace(exception); System.err.println("Unknown File Error"); //TODO: dialog clearJobs(); // Aborting action return; } catch (WriteNotPermittedException exception) { DebugStream.printStackTrace(exception);System.err.println("file not found"); System.err.println("Write not permitted"); //TODO: dialog clearJobs(); // Aborting action return; } //CollectionTreeNode new_target_node = new CollectionTreeNode(target_file); if (overwrite_file == false) { // Add the new node into the tree //FileSystemModel target_model = file_job.target.getTreeModel(); //SynchronizedTreeModelTools.insertNodeInto(target_model, target_node, new_target_node); } //Gatherer.c_man.fireFileAddedToCollection(target_file); // Copy the non-folder level metadata assigned to the original file to the new file if (file_job.type == GWTFileJob.COPY) { // do metadata too //ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToExternalFile(source_file); //MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata); } } private void doFileMove(GWTFileJob file_job) { GWTFileNode source_node = file_job.getOrigin(); GWTFileNode target_node = file_job.getDestination(); File source_file = source_node.getFile(); File target_file = new File(target_node.getFile(), source_file.getName()); if (file_job.type == GWTFileJob.RENAME) { // This is the only difference between moves and renames target_file = target_node.getFile(); target_node = (GWTFileNode) source_node.getParent(); } // Check the target file isn't the source file if (target_file.equals(source_file)) { DebugStream.println("Target file is the source file!"); return; } ///we ALWAYS overwrite, because these files have been pre-resolved // The target file shouldn't already exist /*if (target_file.exists()) { int result = showOverwriteDialog(target_file.getName()); if (result == JOptionPane.NO_OPTION) { // Don't overwrite return; } if (result == JOptionPane.CANCEL_OPTION) { clearJobs(); // Aborting action return; } }*/ // Move the file by renaming it if (!source_file.renameTo(target_file)) { String args[] = { source_file.getName(), target_file.getAbsolutePath() }; System.err.println("Couldn't rename file"); //TODO: dialog clearJobs(); // Aborting action //} return; } //TODO: modify the metadata FM to only take in source files // Remove the node from the source model and add it to the target model //SynchronizedTreeModelTools.removeNodeFromParent(file_job.source.getTreeModel(), source_node); //CollectionTreeNode new_target_node = new CollectionTreeNode(target_file); //FileSystemModel target_model = file_job.target.getTreeModel(); //SynchronizedTreeModelTools.insertNodeInto(target_model, target_node, new_target_node); // Move the non-folder level metadata assigned to the original file to the new file //ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_file); //MetadataXMLFileManager.removeMetadata((CollectionTreeNode) source_node, assigned_metadata); //MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata); } /** all this does is move the metadata, and delete the source */ private void doFileReplace(GWTFileJob file_job) { GWTFileNode source_node = file_job.getOrigin(); GWTFileNode target_node = file_job.getDestination(); File source_file = source_node.getFile(); File target_file = target_node.getFile(); //TODO: metadata part /* // Move the non-folder level metadata assigned to the original file to the new file CollectionTreeNode new_target_node = new CollectionTreeNode(target_file); ArrayList assigned_metadata = MetadataXMLFileManager.getMetadataAssignedDirectlyToFile(source_file); MetadataXMLFileManager.removeMetadata((CollectionTreeNode) source_node, assigned_metadata); MetadataXMLFileManager.addMetadata((CollectionTreeNode) new_target_node, assigned_metadata); */ // now delete the original doFileDelete(file_job); } private void processFileJob(GWTFileJob file_job) { files_in_queue = queue.size(); DebugStream.println("Processing file job " + file_job + "..."); // Ensure that the source file exists File source_file = file_job.getOrigin().getFile(); if (!source_file.exists()) { // The source file doesn't exist, so give the user the option of continuing or cancelling System.err.println("Source file does not exist: cancelling job"); //TODO: some sort of dialog clearJobs(); // Aborting action } // Enable the "Stop" button //TODO: enable client side stop button //stop_button.setEnabled(true); // Delete empty directory job if (file_job.type == GWTFileJob.DELETE_EMPTY_DIRECTORY) { System.err.println("Deleting empty directory..."); doEmptyDirectoryDelete(file_job); return; } // Delete job if (file_job.type == GWTFileJob.DELETE) { System.err.println("Deleting file " + file_job.getOrigin().displayFileName); if (source_file.isFile()) { long source_file_size = source_file.length(); doFileDelete(file_job); done_size += source_file_size; } else { doDirectoryDelete(file_job); } return; } // Copy job if (file_job.type == GWTFileJob.COPY || file_job.type == GWTFileJob.COPY_FILE_ONLY) { System.err.println("Copying file " + source_file.getName() + " to the destination " + file_job.getDestination().file.getAbsolutePath()); //file_status.setText(Dictionary.get("FileActions.Copying", formatPath("FileActions.Copying", source_file.getAbsolutePath(), file_status.getSize().width))); if (source_file.isFile()) { long source_file_size = source_file.length(); doFileCopy(file_job); //done_size += source_file_size; } else { doDirectoryCopy(file_job); } return; } // Move (or rename) job if (file_job.type == GWTFileJob.MOVE || file_job.type == GWTFileJob.RENAME) { System.err.println("Moving file " + source_file.getName() + " to the destination " + file_job.getDestination().file.getAbsolutePath()); //file_status.setText(Dictionary.get("FileActions.Moving", formatPath("FileActions.Moving", source_file.getAbsolutePath(), file_status.getSize().width))); if (source_file.isFile()) { long source_file_size = source_file.length(); doFileMove(file_job); done_size += source_file_size; //progress.addValue(source_file_size); // Update progress bar } else { doDirectoryMove(file_job); } return; } // Replace job if (file_job.type == GWTFileJob.REPLACE) { //file_status.setText(Dictionary.get("FileActions.Replacing", formatPath("FileActions.Replacing", source_file.getAbsolutePath(), file_status.getSize().width))); doFileReplace(file_job); return; } } /*private int showErrorDialog(String error_message) { Object[] options = { Dictionary.get("General.OK"), Dictionary.get("General.Cancel") }; int result = JOptionPane.showOptionDialog(Gatherer.g_man, error_message, Dictionary.get("General.Error"), JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, null, options, options[0]); if (result == 0) { return JOptionPane.OK_OPTION; } else { return JOptionPane.CANCEL_OPTION; } } private int showOverwriteDialog(String target_file_name) { // Has "yes to all" been set? if (yes_to_all) { return JOptionPane.YES_OPTION; } Object[] options = { Dictionary.get("General.Yes"), Dictionary.get("FileActions.Yes_To_All"), Dictionary.get("General.No"), Dictionary.get("General.Cancel") }; int result = JOptionPane.showOptionDialog(Gatherer.g_man, Dictionary.get("FileActions.File_Exists", target_file_name), Dictionary.get("General.Warning"), JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); if (result == 0) { return JOptionPane.YES_OPTION; } else if (result == 1) { yes_to_all = true; return JOptionPane.YES_OPTION; } else if (result == 2) { return JOptionPane.NO_OPTION; } else { return JOptionPane.CANCEL_OPTION; } }*/ public void run() { super.setName("FileQueue"); while (!Gatherer.exit) { // Retrieve the next job int position = queue.size() - 1; if (position >= 0) { // We have a file job, so process it processFileJob((GWTFileJob) queue.remove(position)); } else { // No jobs, so reset and wait until we are notified of one synchronized(this) { // Force both workspace and collection trees to refresh if (Gatherer.g_man != null && Gatherer.c_man.ready()) { Gatherer.g_man.refreshWorkspaceTree(DragTree.COLLECTION_CONTENTS_CHANGED); // Gatherer.g_man.refreshCollectionTree(DragTree.COLLECTION_CONTENTS_CHANGED); } // Reset status area //file_status.setText(Dictionary.get("FileActions.No_Activity")); //progress.reset(); //progress.setString(Dictionary.get("FileActions.No_Activity")); // Reset "yes to all" and "cancel" flags //yes_to_all = false; //cancel_action = false; // Wait for a new file job try { wait(); } catch (InterruptedException exception) {} } } } } /** Register the button that will be responsible for stopping executing file actions. * @param stop_button a JButton */ /*public void registerStopButton(JButton stop_button) { this.stop_button = stop_button; }*/ synchronized private void clearJobs() { queue.clear(); } /** Copy the contents from the source directory to the destination * directory. * @param source The source directory * @param destination The destination directory * @see org.greenstone.gatherer.Gatherer */ public void copyDirectoryContents(File source, File destination, boolean overwrite) throws FileAlreadyExistsException, FileNotFoundException, InsufficientSpaceException, IOException, ReadNotPermittedException, UnknownFileErrorException, WriteNotPermittedException { if (!source.isDirectory()) return; // check that dest dirs exist destination.mkdirs(); File [] src_files = source.listFiles(); if (src_files.length == 0) return; // nothing to copy for (int i=0; i destination.length()) { // Determine the difference (which I guess is in bytes). long difference = (destination_size + (long) data_size) - destination.length(); // Transform that into a human readable string. String message = Utility.formatFileLength(difference); throw new InsufficientSpaceException(message); } else { throw(io_exception); } } allowance--; } if(cancelled) { queue.clear(); job_size = done_size; cancelled = false; //ensure no trace of the file remains: delete it try { destination.delete(); } catch (Exception e) { e.printStackTrace(); } //make sure we close these, or else we lock that file for good! f_in.close(); f_out.close(); } allowance = 0; files_in_queue = queue.size(); queue_free(); monitor.notify(); } // Flush and close the streams to ensure all bytes are written. f_in.close(); f_out.close(); //allowance = 0; // We have now, in theory, produced an exact copy of the source file. Check this by comparing sizes. if(!destination.exists() || (/*!cancel_action && */source.length() != destination.length())) { throw new UnknownFileErrorException(); } // If we were cancelled, ensure that none of the destination file exists. if (cancelled) { destination.delete(); } } }