the.Legend
the.Legend

Reputation: 686

Java delete files unix way (unlink header)

I'm having an issue where archival module written in Java fails to clean files shared via smb if opened by network users during cleanup. Below is simplified version of code for file cleanup:

    private static boolean moveFile(String sourceFilePath, String targetFilePath) {

    boolean fileStatus = false;
    File sourceFile = new File(sourceFilePath );
    File targetFile = new File(targetFilePath );
    if(sourceFile.canRead() && sourceFile.canWrite() ) {
        if(targetFile.exists()) {
            fileStatus = (new File(targetFilePath)).delete();
            if(!fileStatus) {
                Logger.ERROR("Target deletion failed");
            }
        }

        fileStatus = sourceFile.renameTo(new File(targetFilePath));

        if(!fileStatus) {
            Logger.ERROR("RenameTo method failed");
            return false;
        } else {
            Logger.INFO("Move succeeded");
            return true;
        }
    } else {
        Logger.ERROR("Cannot read file");
        return false;
    }
}

It works fine when I test it in two Linux sessions: session A:

cat -v /dev/zero > sourceFile.txt

session B:

java -jar JavaUnixFileRemovalTest.jar sourceFile.txt targetFile.txt

But fails in production when working with network shares and users.

What I'd like to implement instead is to copy file to archive folder and unlink the header. This way if user still has the file opened he'll continue accessing the content, while name is removed from the file system so nobody else can see the file.

So the question is if there's a way to unlink file header in Unix by native Java means without explicitly calling unlink command

Upvotes: 3

Views: 1816

Answers (2)

fjab
fjab

Reputation: 89

File.delete seems to behave as "unlink".

Here's an example


import java.io.*;

public class Main {
    public static void main(String args[]) throws InterruptedException, IOException {
        File invisibleFile = new File("invisiblefile");
        invisibleFile.createNewFile();
        FileWriter fw = new FileWriter(invisibleFile);
        System.out.println("file created");
        Thread.sleep(5000);

        boolean deleted = invisibleFile.delete();
        if(!deleted) {
            System.out.println("error deleting file");
            System.exit(1);
        }
        fw.write("hello");       
        fw.flush();         
        System.out.println("file deleted");
        // 'ls invisiblefile' does not return anything
        // but the file is still held open by the process:
        // lsof -p $(ps -ef | awk '/[I]nvisibleFile.java/ {print $2}') | awk '/invisiblefile/ {print "size:" $7; print "inode:" $8}'
        Thread.sleep(5000);

        fw.close();
        System.out.println("file closed");
        Thread.sleep(5000);
        // after closing file, it is completely gone
        
        System.out.println("end");
    }
}

And here's a terminal session to check the program's behaviour:

23:30:07 % java InvisibleFile.java
file created
^Z
zsh: suspended  java InvisibleFile.java
23:30:11 % ls invisiblefile
invisiblefile
23:30:14 % fg %1
[1]  - continued  java InvisibleFile.java
file deleted
^Z
zsh: suspended  java InvisibleFile.java
23:30:21 % ls invisiblefile
ls: invisiblefile: No such file or directory
23:30:23 % lsof -p $(ps -ef | awk '/[I]nvisibleFile.java/ {print $2}') | awk '/invisiblefile/ {print "size:" $7; print "inode:" $8}'
size:5
inode:33745509
23:30:30 % fg %1
[1]  - continued  java InvisibleFile.java
file closed
^Z
zsh: suspended  java InvisibleFile.java
23:30:37 % lsof -p $(ps -ef | awk '/[I]nvisibleFile.java/ {print $2}') | awk '/invisiblefile/ {print "size:" $7; print "inode:" $8}'
23:30:42 % fg %1
[1]  - continued  java InvisibleFile.java
end
23:30:47 %

Upvotes: 0

the.Legend
the.Legend

Reputation: 686

After some research I decided to approach this problem in a bit different way and cast powerful lost magic of the Ancients - that is, use native system C calls with help of JNA (Java Native Access)

Here's an example of the code with some explanations for JNA first-time users:

package com.WeLoveStackOverflow.JavaJNAUnlinkTest;

import java.io.File;    
import com.sun.jna.Library;
import com.sun.jna.Native;

public class Main {
    private static CStdLib cStdLib;

    // Here you specify prototypes of native C methods to be called during runtime
    // Because unlink(char *path) uses pointer to const char as argument, a wrapper class StringByReference is used to convert data types
    // Link to other examples at the end of this post
    public interface CStdLib extends Library {
        int unlink(StringByReference path);
    }

    public static void main(String[] args) {

        // Here I'm declaring libc usage, but you can link anything. Even your own libraries
        cStdLib = (CStdLib)Native.loadLibrary("c", CStdLib.class);

        Logger.INFO("Source file: " + args[0]);
        Logger.INFO("Target file: " + args[1]);
        moveFile(args[0],args[1]);
    }

    private static boolean moveFile(String sourceFilePath, String targetFilePath) {
        boolean fileStatus = false;
        File sourceFile = new File(sourceFilePath );
        File targetFile = new File(targetFilePath );
        if(sourceFile.canRead() && sourceFile.canWrite() ) {
            if(targetFile.exists()) {
                fileStatus = targetFile.delete();
                if(!fileStatus) {
                    Logger.ERROR("Target deletion failed");
                }
            }

            fileStatus = sourceFile.renameTo(targetFile);

            if(!fileStatus) {
                Logger.ERROR("RenameTo method failed");
                Logger.INFO("Trying to copy file and unlink the original");

                // ToDo: add copy method

                // That's where we convert String to char*
                StringByReference unlinkPath=new StringByReference(sourceFilePath);
                int status=cStdLib.unlink(unlinkPath);

                if(status==0){
                    Logger.INFO("Unlink succeeded");
                }else {
                    Logger.ERROR("Unlink also failed");
                    return false;
                }
            } else {
                Logger.INFO("Move succeeded");
            }
        } else {
            Logger.ERROR("Cannot read file");
            return false;
        }
        return true;
    }
}

And class for converting data types:

package com.WeLoveStackOverflow.JavaJNAUnlinkTest;
import com.sun.jna.ptr.ByReference;

public class StringByReference extends ByReference {
    public StringByReference() {
        this(0);
    }

    public StringByReference(String str) {
        super(str.length() < 4 ? 4 : str.length() + 1);
        setValue(str);
    }

    private void setValue(String str) {
        getPointer().setString(0, str);
    }
}

So what we've got in the end? A nice Java unlink utility! test scenario: create a text file in session A, open it in less in session B and run java code in session A. Works as expected:

[me@server1 JavaFileTest]$ lsof | grep sourceFile
less      12611   me    4r      REG  253,0        0      73 /home/me/JavaFileTest/sourceFile (deleted)

This is the article I used as a reference: http://jnaexamples.blogspot.com/2012/03/java-native-access-is-easy-way-to.html It contains other good examples of wrapping data types for C calls

Tips:

  • Make sure you have both JNA and JNA-platform files in your classpath
  • JNA 4.4.0 requires GLIBC_2.14. If you're getting this error then simply downgrade JNA (4.2.2 worked for me)

    Exception in thread "main" java.lang.UnsatisfiedLinkError: /lib64/libc.so.6: version 'GLIBC_2.14' not found

Upvotes: 2

Related Questions