[a123] showlink.ahk — navigate to hardlinks of file

Post your working scripts, libraries and tools.
Saiapatsu
Posts: 17
Joined: 11 Jul 2019, 15:02

[a123] showlink.ahk — navigate to hardlinks of file

Post by Saiapatsu » 14 Feb 2021, 19:57

I'm putting this here because I found no examples of FindFirstFileNameW and FindNextFileNameW usage in AHK.

Code: Select all

; showlink.ahk <path>
; reveals all locations hardlinked to path
; path must be complete
; does not show target location; shows first location in active explorer window
; opens new explorer window for all other locations
; has nothing to do if the target location is the only location

; take arguments
try targetpath := A_Args.RemoveAt(1)
catch
	MsgBox("No target specified") && ExitApp()

if (SubStr(targetpath, 2, 2) != ":\") ; gotcha: 2 is the length, not the end
	MsgBox("Target path is not complete") && ExitApp()

explorer := GetActiveExplorer()
for path in ListLinks(targetpath)
	(path != targetpath) && explorer := ShowPath(path, explorer)

; ---------------------------------------------------------------------------- ;

ShowPath(path, explorer)
{
	if (explorer)
	{
		; in an existing explorer window, navigate to and select the specified file
		; regex will never fail
		explorer.Navigate(RegExMatch(path, "(.*)\\.*", match) ? match[1] : MsgBox("Regex failure"))
		; it takes some time to navigate and start seeing files
		
		; https www.mrexcel.com /board/threads/select-a-file-in-file-explorer-from-a-list-of-files-in-excel-in-the-same-fe-window.1084715/  Broken Link for safety
		; https://stackoverflow.com/questions/2518257/get-the-selected-file-in-an-explorer-window
		; event-based approach didn't work out, let's spin around in circles until we get it
		success := false
		stopAt := A_TickCount + 3000
		loop
			try explorer.Document.SelectItem(path, 5) || success := true
			catch
				Sleep(10)
		until success || A_TickCount > stopAt
	} else {
		; open a new explorer window with the path selected
		; gotcha: the quotes must start after the comma!
		Run("explorer.exe /select,`"" path "`"")
	}
}

; get currently active explorer window or nothing
; https://autohotkey.com/board/topic/102127-navigating-explorer-directories/
GetActiveExplorer()
{
	WinHWND := WinActive("A") ; Active window
	for Item in ComObjCreate("Shell.Application").Windows
		if (Item.HWND == WinHWND)
			return Item ; Return active window object
}

; enumerate all locations of file
; prepends drive letter before the output paths to make them whole :)
; todo: take advantage of buflen being set to the required length
; https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfilenamew
; https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilenamew
ListLinks(path)
{
	static ERROR_MORE_DATA := 234
	static MAX_PATH := 260
	
	root := SubStr(path, 1, 2)
	paths := []
	
	buflen := MAX_PATH
	VarSetStrCapacity(linkname, buflen)
	handle := DllCall("FindFirstFileNameW",
		"WStr", path,
		"UInt", 0,
		"UInt*", buflen,
		"WStr", linkname)
	
	if (A_LastError == ERROR_MORE_DATA)
		throw "ListLinks: ERROR_MORE_DATA, 260 was not enough..."
	if (handle == 0xffffffff)
		throw "ListLinks: FindFirstFileNameW failed"
	
	try
	{
		Loop
		{
			paths.Push(root linkname)
			
			buflen := MAX_PATH
			VarSetStrCapacity(linkname, buflen)
			more := DllCall("FindNextFileNameW",
			"UInt", handle,
			"UInt*", buflen,
			"WStr", linkname)
		} until (!more)
		
		if (A_LastError == ERROR_MORE_DATA)
			throw "ListLinks: ERROR_MORE_DATA, 260 was not enough..."
	} finally
		DllCall("FindClose", "UInt", handle)
	
	return paths
}
In short, a filename is a reference to a file and file can have many references (filenames). When a file has many filenames, it is said that the filenames are hardlinked to each other. Unlike shortcuts (.lnk files) and symlinks, they are references to a file, whereas the others are references to a filename which is a reference to a file.

I use Link Shell Extension to see and create all of these links natively in Explorer. It's possible to see a list of locations that point to a file from Properties (from a tab added by LSE), but that's very annoying compared to how this script is used.

The script takes one command-line argument, a path to a file.
If the file has no hardlinks (is the only reference to a file), it does nothing.
If the file is hardlinked, it navigates the currently active Explorer window to one of the other locations, and opens new Explorer windows with the other locations highlighted.

To use this script, do anything that would cause the script to be run with the path as its argument.
  • In cmd, run showlink.ahk [file path here]
  • Drag and drop a file onto the script (if you have setup drag and drop onto .ahk files)
  • Create a shortcut to the script and drag and drop a file onto it
etc etc.

My favorite way is to add a Send To link to it.
Go to shell:sendto (which is actually %appdata%\Microsoft\Windows\SendTo) and put a shortcut to this script there.
Now you can right-click any file and send it to this script to navigate to other locations of that file.
I also put &K in my shortcut's name so that I can write Menunk to rightclick a file, select Send to and select showlink.ahk (K).

Hardlinks are useful to me because among other uses, they create a two-way link between two or more locations, united by the underlying file they represent.
For example, I have video/audio files strewn about my disk, with annoying sidecar files. I linked the media files to a central location and moved the sidecars there; or rather, moved all the files to the central location and linked the media to where it's most convenient.
Thus, I am free to rename, move etc. the media file alone, with no clutter beside it and no need to rename many files at a time. I can follow the hardlink to see the sidecars, or go from the central location to the media regardless of where the media is, and furthermore always be able to recover the file if I happen to delete it, because it was never deleted, the file still remains in the central folder.

(One other way to do the above is to embed the sidecar files as Alternate Data Streams, but I'm irrationally scared of them)

User avatar
RobertL
Posts: 546
Joined: 18 Jan 2014, 01:14
Location: China

Re: [a123] showlink.ahk — navigate to hardlinks of file

Post by RobertL » 15 Jun 2021, 02:21

Thanks for sharing.
I think it's much faster than normal way - fsutil hardlink list <filename>.

Although I prefer AHK V2, but now I use V1. Here is some compatible package. It's not strict, only could just run in V1.

Code: Select all

diff --git "a/linkshow.ahk" "b/linkshow.ahk"
index c47f624..6fe4042 100644
--- "a/linkshow.ahk"
+++ "b/linkshow.ahk"
@@ -14,7 +14,7 @@ if (SubStr(targetpath, 2, 2) != ":\") ; gotcha: 2 is the length, not the end
 	MsgBox("Target path is not complete") && ExitApp()
 
 explorer := GetActiveExplorer()
-for path in ListLinks(targetpath)
+for index,path in ListLinks(targetpath)
 	(path != targetpath) && explorer := ShowPath(path, explorer)
 
 ; ---------------------------------------------------------------------------- ;
@@ -25,7 +25,7 @@ ShowPath(path, explorer)
 	{
 		; in an existing explorer window, navigate to and select the specified file
 		; regex will never fail
-		explorer.Navigate(RegExMatch(path, "(.*)\\.*", match) ? match[1] : MsgBox("Regex failure"))
+		explorer.Navigate(RegExMatch(path, "O)(.*)\\.*", match) ? match[1] : MsgBox("Regex failure"))
 		; it takes some time to navigate and start seeing files
 		
 		; https www.mrexcel.com /board/threads/select-a-file-in-file-explorer-from-a-list-of-files-in-excel-in-the-same-fe-window.1084715/  Broken Link for safety
@@ -70,11 +70,11 @@ ListLinks(path)
 	
 	buflen := MAX_PATH
 	VarSetStrCapacity(linkname, buflen)
-	handle := DllCall("FindFirstFileNameW",
-		"WStr", path,
-		"UInt", 0,
-		"UInt*", buflen,
-		"WStr", linkname)
+	handle := DllCall("FindFirstFileNameW"
+		,"WStr", path
+		,"UInt", 0
+		,"UInt*", buflen
+		,"WStr", linkname)
 	
 	if (A_LastError == ERROR_MORE_DATA)
 		throw "ListLinks: ERROR_MORE_DATA, 260 was not enough..."
@@ -89,10 +89,10 @@ ListLinks(path)
 			
 			buflen := MAX_PATH
 			VarSetStrCapacity(linkname, buflen)
-			more := DllCall("FindNextFileNameW",
-			"UInt", handle,
-			"UInt*", buflen,
-			"WStr", linkname)
+			more := DllCall("FindNextFileNameW"
+			,"UInt", handle
+			,"UInt*", buflen
+			,"WStr", linkname)
 		} until (!more)
 		
 		if (A_LastError == ERROR_MORE_DATA)
@@ -102,3 +102,19 @@ ListLinks(path)
 	
 	return paths
 }
+
+ExitApp(){
+	ExitApp
+}
+MsgBox(text){
+	MsgBox % text
+}
+Sleep(delay){
+	Sleep delay
+}
+Run(target){
+	Run % target
+}
+VarSetStrCapacity(ByRef TargetVar,RequestedCapacity){
+	VarSetCapacity(TargetVar,RequestedCapacity)
+}

Code: Select all

; showlink_AHKv1.ahk <path>
; reveals all locations hardlinked to path
; path must be complete
; does not show target location; shows first location in active explorer window
; opens new explorer window for all other locations
; has nothing to do if the target location is the only location

; take arguments
try targetpath := A_Args.RemoveAt(1)
catch
	MsgBox("No target specified") && ExitApp()

if (SubStr(targetpath, 2, 2) != ":\") ; gotcha: 2 is the length, not the end
	MsgBox("Target path is not complete") && ExitApp()

explorer := GetActiveExplorer()
for index,path in ListLinks(targetpath)
	(path != targetpath) && explorer := ShowPath(path, explorer)

; ---------------------------------------------------------------------------- ;

ShowPath(path, explorer)
{
	if (explorer)
	{
		; in an existing explorer window, navigate to and select the specified file
		; regex will never fail
		explorer.Navigate(RegExMatch(path, "O)(.*)\\.*", match) ? match[1] : MsgBox("Regex failure"))
		; it takes some time to navigate and start seeing files
		
		; https www.mrexcel.com /board/threads/select-a-file-in-file-explorer-from-a-list-of-files-in-excel-in-the-same-fe-window.1084715/  Broken Link for safety
		; https://stackoverflow.com/questions/2518257/get-the-selected-file-in-an-explorer-window
		; event-based approach didn't work out, let's spin around in circles until we get it
		success := false
		stopAt := A_TickCount + 3000
		loop
			try explorer.Document.SelectItem(path, 5) || success := true
			catch
				Sleep(10)
		until success || A_TickCount > stopAt
	} else {
		; open a new explorer window with the path selected
		; gotcha: the quotes must start after the comma!
		Run("explorer.exe /select,`"" path "`"")
	}
}

; get currently active explorer window or nothing
; https://autohotkey.com/board/topic/102127-navigating-explorer-directories/
GetActiveExplorer()
{
	WinHWND := WinActive("A") ; Active window
	for Item in ComObjCreate("Shell.Application").Windows
		if (Item.HWND == WinHWND)
			return Item ; Return active window object
}

; enumerate all locations of file
; prepends drive letter before the output paths to make them whole :)
; todo: take advantage of buflen being set to the required length
; https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfilenamew
; https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilenamew
ListLinks(path)
{
	static ERROR_MORE_DATA := 234
	static MAX_PATH := 260
	
	root := SubStr(path, 1, 2)
	paths := []
	
	buflen := MAX_PATH
	VarSetStrCapacity(linkname, buflen)
	handle := DllCall("FindFirstFileNameW"
		,"WStr", path
		,"UInt", 0
		,"UInt*", buflen
		,"WStr", linkname)
	
	if (A_LastError == ERROR_MORE_DATA)
		throw "ListLinks: ERROR_MORE_DATA, 260 was not enough..."
	if (handle == 0xffffffff)
		throw "ListLinks: FindFirstFileNameW failed"
	
	try
	{
		Loop
		{
			paths.Push(root linkname)
			
			buflen := MAX_PATH
			VarSetStrCapacity(linkname, buflen)
			more := DllCall("FindNextFileNameW"
			,"UInt", handle
			,"UInt*", buflen
			,"WStr", linkname)
		} until (!more)
		
		if (A_LastError == ERROR_MORE_DATA)
			throw "ListLinks: ERROR_MORE_DATA, 260 was not enough..."
	} finally
		DllCall("FindClose", "UInt", handle)
	
	return paths
}

ExitApp(){
	ExitApp
}
MsgBox(text){
	MsgBox % text
}
Sleep(delay){
	Sleep delay
}
Run(target){
	Run % target
}
VarSetStrCapacity(ByRef TargetVar,RequestedCapacity){
	VarSetCapacity(TargetVar,RequestedCapacity)
}
And, maybe, some adjust needed on path in Run("explorer.exe /select,`"" path "`""), to quote path with double quotation marks for supporting space-hole in it.

Because Path is not case-sensitive, so before comparing, could convert them both to upper/lower case.
path:=Format("{:U}",path)
我为人人,人人为己?

Post Reply

Return to “Scripts and Functions (v2)”