|
@@ -0,0 +1,214 @@
|
|
|
+/**
|
|
|
+ * Licensed to the Apache Software Foundation (ASF) under one
|
|
|
+ * or more contributor license agreements. See the NOTICE file
|
|
|
+ * distributed with this work for additional information
|
|
|
+ * regarding copyright ownership. The ASF licenses this file
|
|
|
+ * to you under the Apache License, Version 2.0 (the
|
|
|
+ * "License"); you may not use this file except in compliance
|
|
|
+ * with the License. You may obtain a copy of the License at
|
|
|
+ *
|
|
|
+ * http://www.apache.org/licenses/LICENSE-2.0
|
|
|
+ *
|
|
|
+ * Unless required by applicable law or agreed to in writing, software
|
|
|
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
+ * See the License for the specific language governing permissions and
|
|
|
+ * limitations under the License.
|
|
|
+ */
|
|
|
+package org.apache.hadoop.fs;
|
|
|
+
|
|
|
+import java.text.*;
|
|
|
+import java.io.*;
|
|
|
+import java.net.URI;
|
|
|
+import java.util.Date;
|
|
|
+
|
|
|
+import org.apache.commons.logging.*;
|
|
|
+
|
|
|
+import org.apache.hadoop.conf.*;
|
|
|
+
|
|
|
+/** Provides a <i>trash</i> feature. Files may be moved to a trash directory.
|
|
|
+ * They're initially stored in a <i>current</i> sub-directory of the trash
|
|
|
+ * directory. Within that sub-directory their original path is preserved.
|
|
|
+ * Periodically one may checkpoint the current trash and remove older
|
|
|
+ * checkpoints. (This design permits trash management without enumeration of
|
|
|
+ * the full trash content, without date support in the filesystem, and without
|
|
|
+ * clock synchronization.)
|
|
|
+ */
|
|
|
+public class Trash extends Configured {
|
|
|
+ private static final Log LOG =
|
|
|
+ LogFactory.getLog("org.apache.hadoop.fs.Trash");
|
|
|
+
|
|
|
+ private static final String CURRENT = "Current";
|
|
|
+ private static final DateFormat CHECKPOINT = new SimpleDateFormat("yyMMddHHmm");
|
|
|
+ private static final int MSECS_PER_MINUTE = 60*1000;
|
|
|
+
|
|
|
+ private FileSystem fs;
|
|
|
+ private Path root;
|
|
|
+ private Path current;
|
|
|
+ private long interval;
|
|
|
+
|
|
|
+ /** Construct a trash can accessor.
|
|
|
+ * @param conf a Configuration
|
|
|
+ */
|
|
|
+ public Trash(Configuration conf) throws IOException {
|
|
|
+ super(conf);
|
|
|
+
|
|
|
+ Path root = new Path(conf.get("fs.trash.root", "/tmp/Trash"));
|
|
|
+
|
|
|
+ this.fs = root.getFileSystem(conf);
|
|
|
+
|
|
|
+ if (!root.isAbsolute())
|
|
|
+ root = new Path(fs.getWorkingDirectory(), root);
|
|
|
+
|
|
|
+ this.root = root;
|
|
|
+ this.current = new Path(root, CURRENT);
|
|
|
+ this.interval = conf.getLong("fs.trash.interval", 60) * MSECS_PER_MINUTE;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Move a file or directory to the current trash directory.
|
|
|
+ * @return false if the item is already in the trash or trash is disabled
|
|
|
+ */
|
|
|
+ public boolean moveToTrash(Path path) throws IOException {
|
|
|
+ if (interval == 0)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ if (!path.isAbsolute()) // make path absolute
|
|
|
+ path = new Path(fs.getWorkingDirectory(), path);
|
|
|
+
|
|
|
+ if (!fs.exists(path)) // check that path exists
|
|
|
+ throw new FileNotFoundException(path.toString());
|
|
|
+
|
|
|
+ URI rootUri = root.toUri();
|
|
|
+ String dirPath = path.toUri().getPath();
|
|
|
+
|
|
|
+ if (dirPath.startsWith(rootUri.getPath())) { // already in trash
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ Path trashPath = // create path in current
|
|
|
+ new Path(rootUri.getScheme(), rootUri.getAuthority(),
|
|
|
+ current.toUri().getPath()+dirPath);
|
|
|
+
|
|
|
+ IOException cause = null;
|
|
|
+
|
|
|
+ // try twice, in case checkpoint between the mkdirs() & rename()
|
|
|
+ for (int i = 0; i < 2; i++) {
|
|
|
+ Path trashDir = trashPath.getParent();
|
|
|
+ if (!fs.mkdirs(trashDir)) { // make parent directory
|
|
|
+ throw new IOException("Failed to create trash directory: "+trashDir);
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ if (fs.rename(path, trashPath)) // move to current trash
|
|
|
+ return true;
|
|
|
+ } catch (IOException e) {
|
|
|
+ cause = e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw (IOException)
|
|
|
+ new IOException("Failed to move to trash: "+path).initCause(cause);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Create a trash checkpoint. */
|
|
|
+ public void checkpoint() throws IOException {
|
|
|
+ if (!fs.exists(current)) // no trash, no checkpoint
|
|
|
+ return;
|
|
|
+
|
|
|
+ Path checkpoint;
|
|
|
+ synchronized (CHECKPOINT) {
|
|
|
+ checkpoint = new Path(root, CHECKPOINT.format(new Date()));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fs.rename(current, checkpoint)) {
|
|
|
+ LOG.info("Created trash checkpoint: "+checkpoint);
|
|
|
+ } else {
|
|
|
+ throw new IOException("Failed to checkpoint trash: "+checkpoint);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Delete old checkpoints. */
|
|
|
+ public void expunge() throws IOException {
|
|
|
+ Path[] dirs = fs.listPaths(root); // scan trash sub-directories
|
|
|
+ long now = System.currentTimeMillis();
|
|
|
+ for (int i = 0; i < dirs.length; i++) {
|
|
|
+ Path dir = dirs[i];
|
|
|
+ String name = dir.getName();
|
|
|
+ if (name.equals(CURRENT)) // skip current
|
|
|
+ continue;
|
|
|
+
|
|
|
+ long time;
|
|
|
+ try {
|
|
|
+ synchronized (CHECKPOINT) {
|
|
|
+ time = CHECKPOINT.parse(name).getTime();
|
|
|
+ }
|
|
|
+ } catch (ParseException e) {
|
|
|
+ LOG.warn("Unexpected item in trash: "+dir+". Ignoring.");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ((now - interval) > time) {
|
|
|
+ if (fs.delete(dir)) {
|
|
|
+ LOG.info("Deleted trash checkpoint: "+dir);
|
|
|
+ } else {
|
|
|
+ LOG.warn("Couldn't delete checkpoint: "+dir+" Ignoring.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Return a {@link Runnable} that periodically empties the trash.
|
|
|
+ * Only one checkpoint is kept at a time.
|
|
|
+ */
|
|
|
+ public Runnable getEmptier() {
|
|
|
+ return new Emptier();
|
|
|
+ }
|
|
|
+
|
|
|
+ private class Emptier implements Runnable {
|
|
|
+
|
|
|
+ public void run() {
|
|
|
+ if (interval == 0)
|
|
|
+ return; // trash disabled
|
|
|
+
|
|
|
+ long now = System.currentTimeMillis();
|
|
|
+ long end = ceiling(now, interval);
|
|
|
+ while (true) {
|
|
|
+ try { // sleep for interval
|
|
|
+ Thread.sleep(end - now);
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ return; // exit on interrupt
|
|
|
+ }
|
|
|
+
|
|
|
+ now = System.currentTimeMillis();
|
|
|
+ if (now >= end) {
|
|
|
+
|
|
|
+ try {
|
|
|
+ expunge();
|
|
|
+ } catch (IOException e) {
|
|
|
+ LOG.warn("Trash expunge caught: "+e+". Ignoring.");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ checkpoint();
|
|
|
+ } catch (IOException e) {
|
|
|
+ LOG.warn("Trash checkpoint caught: "+e+". Ignoring.");
|
|
|
+ }
|
|
|
+
|
|
|
+ end = ceiling(now, interval);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private long ceiling(long time, long interval) {
|
|
|
+ return floor(time, interval) + interval;
|
|
|
+ }
|
|
|
+ private long floor(long time, long interval) {
|
|
|
+ return (time / interval) * interval;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Run an emptier.*/
|
|
|
+ public static void main(String[] args) throws Exception {
|
|
|
+ new Trash(new Configuration()).getEmptier().run();
|
|
|
+ }
|
|
|
+
|
|
|
+}
|