Page 1 of 1

Lazarus Pascal – Move Items to Trash for Windows, Linux and macOS

Lazarus Pascal – Move Items to Trash for Windows, Linux and macOS
   2

In this short article I’ll show how I implemented a cross platform (Windows, Linux, macOS) function for Lazarus Pascal to move files or directories to the TrashCan or Recycle Bin.

Since it took me some figuring out, I figured; why not write it down so others may benefit from this as well.
For your convenience: You can also download the Lazarus Pascal unit here, which holds the function for macOS, Linux and macOS.




How a computer TrashCan works

Just a quick and short explanation how a TrashCan (Trash, Recycle Bin, BitBucket, etc) works:

The objects (files/directories) you’d like to delete are simply moved to a specific directory on your computer.

Sometimes this is a standard or commonly used directory which may or may not be by the Operating System.

Besides just dumping the object in that directory, certain systems maintain additional data so a restore from the TrashCan can be done.
After all: just moving an object into a TrashCan doesn’t tell the system where it originally came from – in case the user asks to restore the object to its original location.

 

Security – A TrashCan should be private … 

For security and privacy reasons, the TrashCan or Recycle Bin should be accessible only by the specific user.
You would never want other users to have access to your Trashcan and have them dig through your Trash, and make private matters public.

TrashCan not used in Shells, Terminals, etc … 

The TrashCan is often only used in the graphical interface of your system.
Deleting files or directories in a Shell or Terminal window will usually not be using the Trashcan.

 

Move objects to the macOS Trash

With recent macOS versions, the Trashcan can be found in the user’s home directory:  ~/.Trash

So we could do a move manually, but we don’t have to … the macOS API has a function for that: I have used the recycleURLs:completionHandler:.
Obviously we will need de macOS units, but we also need to tell the Free Pascal compiler to switch to ObjectiveC mode.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{$modeswitch ObjectiveC1}
{$linkframework CoreFoundation}

interface

uses
  ... MacOSAll, CocoaAll ...

implementation

function MoveToTrash(FileOrDirName:AnsiString): boolean;
var
  aNSArray : NSMutableArray;
  aNSURL   : NSURL;
begin
  aNSArray := NSMutableArray(NSMutableArray.array_).init; // How can I create a new array?
  aNSURL   := NSURL.fileURLWithPath(NSSTR(pchar(FileOrDirName)));
  aNSArray.addObject(aNSURL);
  NSWorkspace.sharedWorkspace.recycleURLs_completionHandler(aNSArray,nil);
  Result:=true; // recycleURLs doesn't return a result unless we make a function for it.
end;

Please note: Result will always be TRUE … 

Since the recycleURLs function does not return a result, unless we create a callback function for that (replacing the nil parameter), making this too complicated for this article. We will just assume it worked.
Checking if file or directory has been removed is not very reliable either, since the recycleURLs function works async, so the file or directory may be removed at a slightly later time. So a check wouldn’t work.

 

Move objects to the Windows Recycle Bin

Windows, since it is a Microsoft product, has changed the location of the Recycle Bin directory over the years (this CCleaner article is a good read).

For example:
 C:\$Recycle.Bin  for Windows Vista, and newer,
 C:\recycler  for Windows 2000, NT, and XP, or
 C:\recycled  for Windows 95 and 98.

You’d expect  C:\$Recycle.Bin to work for Windows 10 as well, but … it doesn’t.
This honestly makes sense, since a Trashcan should be based on the user and not on a single disk drive.

The good thing I found though is that Windows does have an API call for that and in the Lazarus Forum I found a super helpful post by a user aSerge handling this just fine for me.
It uses the function SHFileOperation Windows ShellAPI and supports both Files and Directories, so we’d need to include the “ShellApi” unit in our uses clause:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
uses ... ShellApi ...

...

function MoveToTrash(const FileOrDirName: UnicodeString): Boolean;
var
  R: TSHFILEOPSTRUCTW;
begin
  FillChar(R, SizeOf(R), 0);
  R.wFunc := FO_DELETE;
  R.pFrom := PWideChar(FileOrDirName + #0);
  R.fFlags := FOF_ALLOWUNDO or FOF_NOCONFIRMATION;
  Result := SHFileOperationW(@R) = 0;
end;   

 

Move objects to the Linux TrashCan

Linux doesn’t seem to have a standard location or API call for using the Trashcan, although there is an “official” TrashCan specification which most distros actually follow.
With the freedom we can find in Linux, this may or may not be implemented, in part or completely, based to these specifications. I suppose this is the strength and achilles heel of Linux.

Linux TrashCan directory location

The specifications suggest reading the value of the environment variable “$XDG_DATA_HOME“, but from experience I found this variable to be missing too often.

I did find however that most of the Linux and FreeBSD variants use  ~/.local/share/Trash/ .
Now with the use of that directory, you will typically see two subdirectories:
 ~/.local/share/Trash/files/ where we store the actual file or directory
 ~/.local/share/Trash/info/ where a reference document is created, which hold restore information.

If this would be all, then I’d be happy, but unfortunately, I have seen quite a few variants as well:
 ~/.local/share/Trash/ ,
 ~/Desktop/Trash/ ,
 ~/trash ,
 ~/Trash ,
 ~/.trash ,
 ~/.Trash

If anyone is aware of other directories, then please let me know by posting a comment below, so I can add it to the list.

So to create a function for Linux as well, I followed this approach:

  1. If  ~/.local/share/Trash/  exists, then use this directory and add the restore file as explained below.
  2. If the directory does not exist, check if one of the other directories exist and just move the file there and do not add the restore information file.
  3. If the alternative directories do not exists then we will create the  ~/.local/share/Trash/ and work as defined in the standard (option 1).

I’m pretty sure someone will blow up over this approach, but please feel free to let me know if you have a better idea or approach. I’d like to learn as well .

So … we will follow the standard if the standard directory  ~/.local/share/Trash/ was found, or if the alternatives were not found. We will move the file or directory into  ~/.local/share/Trash/files/ and a reference document into  ~/.local/share/Trash/info/.
If the standard directory was not found, but one of the alternatives was found, then we move file or directory to that alternative directory. We will however not create a reference document.

Linux TrashCan Filename numbering

If the file already exists (since you deleted a file with the same name before), a digit will be added to distinguish them and not overwrite them.
This applies to the “files” directory and the “info” directory.

The number is basically following this pattern: example.doc, example.2.doc, example.3.doc, etc.
You may have noticed that the first one does not have a “.1” and this is intentional.

Linux TrashCan Reference file (.trashinfo)

In my function, I will create such a file when using the standard TrashCan definition ( ~/.local/share/Trash/).
For the alternative (non-standard) directories we will not create such a file.

The additionally reference file will be created in “~/.local/share/Trash/info/” which includes the original filename and location, and deletion date.
The file has the same filename as the one in “~/.local/share/Trash/files/” with the additional “.trashinfo” extension, and does follow the same numbering as well.

Note: the original name is an escaped URL formatter string, so for example a space is replaced by a %20, like we see with URLs.

The content of this reference file will looks something like this:


1
2
3
[Trash Info]
Path=/path/to/original/location/deletefile.extension
DeletionDate=2020-01-09T17:05:41

Not all Linux Distros observe the .trashinfo file! 

Even though the .trashinfo files are supposed to be observed to help restore files in the TrashCan, not every Linux distro observes this.
For example, LinuxMint 19 (Cinamon) totally ignores it and may rely on a custom file found in “gvfs-metadata”.

My unit will ignore oddballs like that and the user will have to manually drag the file or directory out of the TrashCan.

Linux TrashCan Function in Pascal

Now since we do not have a clean method/API call, I’ve created this approach:

  1. First we will try to rename the file which (depending on the OS) will “move” the file to the destination directory (the TrashCan directory).
  2. If we see that step 1 failed, we will try to copy the file or directory to the TrashCan directory.
  3. If step 2 failed, then we will undo the copying and report back that the move to the TrashCan failed (FALSE).
  4. If step 2 succeeded, then we will delete the original files and we’re done.

So all these steps make the function a lot bigger than the ones we have seen for Windows or macOS as you can see below:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
uses
  ... DateUtils, LCLProc, LazFileUtils, FileUtil ...

...


function MoveToTrash(FileOrDirName:AnsiString): boolean;
var
  TrashCanBase  : string = '~/.local/share/Trash/';
  TrashCanFiles : string = '~/.local/share/Trash/files/';
  TrashCanInfo  : string = '~/.local/share/Trash/info/';
  TrashCanAlt   : string;
  InfoFile      : TStringList;
  isDirectory               : boolean;
  newFileOrDirName          : string;
  NamePart1, NamePart2      : string;
  FilesInDirectory          : TStringList;
  counter                   : integer;

  function EscapeURLString(s: string): string;
  var
    i: integer;
    source: PAnsiChar;
  begin
    Result := '';
    source := pansichar(s);
    for i := 1 to length(source) do
      if not (source[i - 1] in ['A'..'Z', 'a'..'z', '0'..'9', '-', '_', '~', '.', ':', '/']) then
        Result := Result + '%' + IntToHex(Ord(source[i - 1]), 2)
      else
        Result := Result + source[i - 1];
  end;

begin
  if RightStr(FileOrDirName,1)=DirectorySeparator then  // remove directory separator if this is the last char
    FileOrDirName := LeftStr(FileOrDirName,Length(FileOrDirName)-1);

  // Are we moving a directory into the trashcan?
  isDirectory   := DirectoryExists(FileOrDirName);

  TrashCanFiles := ExpandFileName(TrashCanFiles);
  TrashCanInfo  := ExpandFileName(TrashCanInfo);
  TrashCanBase  := ExpandFileName(TrashCanBase);

  if not DirectoryExists(TrashCanBase) then  // We didn't find the standard dir, so now we are guessing alternatives
    begin
      TrashCanAlt := '';

      if DirectoryExists(ExpandFileName('~/Desktop/Trash/')) then
        TrashCanAlt:=ExpandFileName('~/Desktop/Trash/')
      else if DirectoryExists(ExpandFileName('~/trash/')) then
        TrashCanAlt:=ExpandFileName('~/trash/')
      else if DirectoryExists(ExpandFileName('~/Trash/')) then
        TrashCanAlt:=ExpandFileName('~/Trash/')
      else if DirectoryExists(ExpandFileName('~/.trash/')) then
        TrashCanAlt:=ExpandFileName('~/.trash/')
      else if DirectoryExists(ExpandFileName('~/.Trash/')) then
        TrashCanAlt:=ExpandFileName('~/.Trash/');

      if TrashCanAlt='' then  // we didn't find an alternative: create standard dirs
        begin
           ForceDirectories(TrashCanFiles);
           ForceDirectories(TrashCanInfo);
        end
      else                   // we did find an alternative, make sure we use it for files (and not for trashinfo)
        begin
          TrashCanFiles := TrashCanAlt;
          TrashCanInfo  := '';
        end;
    end;

  newFileOrDirName := ExtractFileName(FileOrDirName);
  NamePart1 := LazFileUtils.ExtractFileNameWithoutExt(newFileOrDirName);
  NamePart2 := ExtractFileExt(newFileOrDirName);
  counter   := 0;

  // if file already in Trashcan, we add a number (eg. example.doc, example.2.doc, expample.3.doc etc.)
  while (isDirectory      and DirectoryExists(TrashCanFiles+newFileOrDirName)) or
        (not(isDirectory) and FileExists(TrashCanFiles+newFileOrDirName)) do
    begin
      inc(Counter);
      newFileOrDirName := NamePart1 + BoolToStr(Counter>1,'.'+IntToStr(Counter),'') + NamePart2;
    end;

  // Create a trashinfo file, if the standard dir did exist!
  if TrashCanInfo<>'' then
    begin
      InfoFile := TStringList.Create;
      InfoFile.Text := '[Trash Info]'+LineEnding+
                       'Path='+EscapeURLString(FileOrDirName)+LineEnding+
                       'DeletionDate='+FormatDateTime('YYYY-MM-DD',Now)+'T'+FormatDateTime('hh:nn:ss',Now)+LineEnding; //'2020-01-09T17:05:41'+
      InfoFile.SaveToFile(TrashCanInfo+newFileOrDirName+'.trashinfo');
      InfoFile.Free;
    end;

  // make new filename now full path
  newFileOrDirName := TrashCanFiles+newFileOrDirName;

  // Move File or Directory - try rename first, if that fails try copying files, and if copying worked delete originals
  Result := RenameFile(FileOrDirName,newFileOrDirName);  // try moving file or dir

  if not Result then // if rename failed then try copy and delete (aka move)
    begin
      if isDirectory then
        begin
          FilesInDirectory := FindAllFiles(FileOrDirName, '*', true);

          for Counter:=0 to FilesInDirectory.Count-1 do  // Copy the file structure to the trashcan
            begin
              if ForceDirectories( ExtractFilePath( StringReplace(FilesInDirectory.Strings[Counter],FileOrDirName,newFileOrDirName,[] ) ) ) then
                begin
                  if not CopyFile( FilesInDirectory.Strings[Counter], StringReplace(FilesInDirectory.Strings[Counter],FileOrDirName,newFileOrDirName,[] ) ) then
                    begin  // failed copying file - cleanup and bail
                      Result := false;

                      if DeleteDirectory(newFileOrDirName,true) then // delete what we have copied so far - things are going side ways
                        RemoveDir(newFileOrDirName);

                      if FileExists(TrashCanInfo+newFileOrDirName+'.trashinfo') then
                        DeleteFile(TrashCanInfo+newFileOrDirName+'.trashinfo');

                      Exit;
                    end;
                end
              else // failed creating dir - cleanup and bail
                begin
                  Result := false;

                  if DeleteDirectory(newFileOrDirName,true) then // delete what we have copied so far - things are going sideways
                    RemoveDir(newFileOrDirName);

                      if FileExists(TrashCanInfo+newFileOrDirName+'.trashinfo') then
                        DeleteFile(TrashCanInfo+newFileOrDirName+'.trashinfo');

                  Exit;
                end;
            end;

          // all went well, remove old dir
          if DeleteDirectory(FileOrDirName,true) then
            Result := RemoveDir(FileOrDirName);

          FilesInDirectory.Free;
        end
      else
        begin
          if CopyFile(FileOrDirName,newFileOrDirName,true,false) then         // If rename fails: copy file
            Result := DeleteFile(FileOrDirName)                               // If copy succeeded: remove original
          else
            Result := false;                                                  // Copy/Delete failed d
        end;
    end;
end;

 

 

Cross Platform Unit – My MoveToTrash unit

I’d honestly would expect this to already exist somewhere, somehow, … but I for sure could not find it.
So here you can download my unit to covers Windows, Linux and macOS, for moving files or directories to the TrashCan, Recycle Bin, Bit Bucket, or whatever it is called.

I’m sure there is plenty of room for improvements; please feel free to post them in the comments below.

Here you can download the entire unit:

DOWNLOAD - Lazarus-Pascal-TrashCanUnit.zip 

Filename:  Lazarus-Pascal-TrashCanUnit.zip
Platform:  Undefined
Version:  1.0
Size:  3.5 kB
Date:  2020-01-11

Direct reference link:  https://www.tweaking4all.com/downloads/Lazarus-Pascal-TrashCanUnit.zip
 Download Now 

Donation options


Donations are very much appreciated, but not required. Donations will be used for web-hosting expenses, project hardware or a motivational boost (a drink or snack). Thank you very much for those have donated already! It's truly AwEsOmE to see that folks like our articles and small applications.

Please note that clicking affliate links, like the ones from Amazon, may result in a small commission for us - which we highly appreciate as well.

Comments


There are 2 comments. You can read them below.
You can post your own comments by using the form below, or reply to existing comments by using the "Reply" button.

  • Jan 22, 2020 - 8:47 PM - Johnny7 Comment Link

    Thanks for with this writeup! 

    It saved me a lot of work.

    Reply

    Johnny7

    • Jan 23, 2020 - 4:17 AM - Hans - Author: Comment Link

      Thanks Johnny7!

      Glad to hear it has been of use for you, and I very much appreciate you taking the time to post a Thank You 

      Reply

      Hans



Your Comment …

Do not post large files here (like source codes, log files or config files). Please use the Forum for that purpose.

Please share:
*
*
Notify me about new comments (email).
       You can also use your RSS reader to track comments.


Tweaking4All uses the free Gravatar service for Avatar display.
Tweaking4All will never share your email address with others.