Windows: How to canonicalize a file to the special folder? - windows

i want to to persist some filenames for the user (e.g. recent files).
Let's use six example files:
c:\Documents & Settings\Ian\My Documents\Budget.xls
c:\Documents & Settings\Ian\My Documents\My Pictures\Daughter's Winning Goal.jpg
c:\Documents & Settings\Ian\Application Data\uTorrent
c:\Documents & Settings\All Users\Application Data\Consonto\SpellcheckDictionary.dat
c:\Develop\readme.txt
c:\Program Files\Adobe\Reader\WhatsNew.txt
i'm now hard-coding path to special folders. If the user redirects their folders, roams to another computer, or upgrades their operating system, the paths will be broken:
i want to be a good developer, and convert these hard-coded absolute paths to relative paths from the appropriate special folders:
%CSIDL_Personal%\Budget.xls
%CSIDL_MyPictures%\Daughter's Winning Goal.jpg
%CSIDL_AppData%\uTorrent
%CSIDL_Common_AppData%\Consonto\SpellcheckDictionary.dat
c:\Develop\readme.txt
%CSIDL_Program_Files%\Adobe\Reader\WhatsNew.txt
The difficulty comes with the fact that there can be multiple representations for the same file, e.g.:
c:\Documents & Settings\Ian\My Documents\My Pictures\Daughter's Winning Goal.jpg
%CSIDL_Profile%\My Documents\My Pictures\Daughter's Winning Goal.jpg
%CSIDL_Personal%\My Pictures\Daughter's Winning Goal.jpg
%CSIDL_MyPictures%\Daughter's Winning Goal.jpg
Note also that in Windows XP My Pictures are stored in My Documents:
%CSIDL_Profile%\My Documents
%CSIDL_Profile%\My Documents\My Pictures
But on Vista/7 they are separate:
%CSIDL_Profile%\Documents
%CSIDL_Profile%\Pictures
Note: i realize the syntax
%CSIDL_xxx%\filename.ext is not valid; that
Windows will not expand those keywords
like they are environment strings. i'm only
using it as a way to ask this
question. Internally i would obviously
store the items some other way, perhaps as a CSIDL parent
and the tail of the path, e.g.:
CSIDL_Personal \Budget.xls
CSIDL_MyPictures \Daughter's Winning Goal.jpg
CSIDL_AppData \uTorrent
CSIDL_Common_AppData \Consonto\SpellcheckDictionary.dat
-1 c:\Develop\readme.txt (-1, since 0 is a valid csidl)
CSIDL_Program_Files \Adobe\Reader\WhatsNew.txt
The question becomes, how to use, as much as possible, paths relative to canonical special folders?
I'm thinking:
void CanonicalizeSpecialPath(String path, ref CSLID cslid, ref String relativePath)
{
return "todo";
}
See also
MSDN: CSIDL Enumeration
New Old Thing: Beware of roaming user profiles
New Old Thing: Beware of redirected folders, too
MSDN: PathCanonicalize Function

I suppose you could find out how the CSIDL map to paths (using something like SHGetKnownFolderPath), build a reverse dictionary of them, then check whether the beginning of the path you want to store matches any of the keys in the dictionary and then remove the beginning and store the CSIDL that matched.
Not overtly elegant, but it should get the work done.

function CanonicalizeSpecialPath(const path: string): string;
var
s: string;
BestPrefix: string;
BestCSIDL: Integer;
i: Integer;
begin
BestPrefix := ''; //Start with no csidl being the one
BestCSIDL := 0;
//Iterate over the csidls i know about today for Windows XP.
for i := Low(csidls) to High(csidls) do
begin
//Get the path of this csidl. If the OS doesn't understand it, it returns blank
s := GetSpecialFolderPath(0, i, False);
if s = '' then
Continue;
//Don't do a string search unless this candidate is larger than what we have
if (BestPrefix='') or (Length(s) > Length(BestPrefix)) then
begin
//The special path must be at the start of our string
if Pos(s, Path) = 1 then //1=start
begin
//This is the best csidl we have so far
BestPrefix := s;
BestCSIDL := i;
end;
end;
end;
//If we found nothing useful, then return the original string
if BestPrefix = '' then
begin
Result := Path;
Exit;
end;
{
Return the canonicalized path as pseudo-environment string, e.g.:
%CSIDL_PERSONAL%\4th quarter.xls
}
Result := '%'+CsidlToStr(BestCSIDL)+'%'+Copy(Path, Length(BestPrefix)+1, MaxInt);
end;
And then there's a function that "expands" the special environment keywords:
function ExpandSpecialPath(const path: string): string;
begin
...
end;
which expands:
%CSIDL_PERSONAL%\4th quarter.xls
into
\\RoamingProfileServ\Users\ian\My Documents\4th quarter.xls
It does it by looking for %xx% at the start of the string, and expanding it.

Related

Delphi: how can i get list of running applications with starting path?

Using Delphi (windows app) i want to get list of other applications running currently. Here How to check if a process is running using Delphi? i've found great tutorial about geting filenames/names of running application, however it gives names only process name (for example NOTEPAD.EXE). I've used naturally part with
UpperCase(ExtractFileName(FProcessEntry32.szExeFile))
and
UpperCase(ExtractFilePath(FProcessEntry32.szExeFile))
and just
UpperCase(FProcessEntry32.szExeFile)
but obviously FProcessEntry32.szExeFile does not have a path to file/process
Is there a simply way of getting list with paths? Here's How to get the list of running processes including full file path? solution with JclSysInfo library, but i cant use it in place of work in project.
I looked at what I could in Google and what I found usually concerned just the application that is running or the application that is active, but I can't just find a list of all running applications. Maybe i'm missing something obvious?
I'm not looking for any complex procedures, I'm not much interested in process parrent, or if there is no access to the process path, I don't have it and don't bother.
Any simple hint?
OK, due to helpfull comment from #TLama i've combined topics above to take name and path of process:
function processExists(exeFileName: string): Boolean;
var
ContinueLoopP, ContinueLoopM: BOOL;
FSnapshotHandle1, FSnapshotHandle2: THandle;
FProcessEntry32: TProcessEntry32;
FMODULEENTRY32: TMODULEENTRY32;
begin
FSnapshotHandle1 := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
FProcessEntry32.dwSize := SizeOf(FProcessEntry32);
FMODULEENTRY32.dwSize := SizeOf(FMODULEENTRY32);
ContinueLoopP := Process32First(FSnapshotHandle1, FProcessEntry32);
ContinueLoopM := Module32First(FSnapshotHandle2, FMODULEENTRY32);
Result := False;
while Integer(ContinueLoopP) <> 0 do
begin
if ((UpperCase(ExtractFileName(FProcessEntry32.szExeFile)) =
UpperCase(ExeFileName)) or (UpperCase(FProcessEntry32.szExeFile) =
UpperCase(ExeFileName))) then
Result := True;
ShowMessage(FMODULEENTRY32.szExePath + FProcessEntry32.szExeFile);
ContinueLoopP := Process32Next(FSnapshotHandle1, FProcessEntry32);
ContinueLoopM := Module32Next(FSnapshotHandle2, FMODULEENTRY32);
end;
CloseHandle(FSnapshotHandle1);
CloseHandle(FSnapshotHandle2);
end;
But still FProcessEntry32.szExeFile returns empty string. What i'm doing wrong? Thank You in advance.
I cannot write comment (low score), so I need to write as "answer". Try this code,
using FProcessEntry32.th32ProcessID as parameter:
Function QueryFullProcessImageNameW(hProcess:THandle; dwFlags:Cardinal; lpExeName:PWideChar; Var lpdwSize:Cardinal) : Boolean; StdCall; External 'Kernel32.dll' Name 'QueryFullProcessImageNameW';
Function GetFullPath(Pid:Cardinal) : UnicodeString;
Var rLength:Cardinal;
Handle:THandle;
Begin Result:='';
Handle:=OpenProcess(PROCESS_QUERY_INFORMATION, False, Pid);
If Handle = INVALID_HANDLE_VALUE Then Exit;
rLength:=256; // allocation buffer
SetLength(Result, rLength+1); // for trailing space
If Not QueryFullProcessImageNameW(Handle, 0, #Result[1],rLength) Then Result:='' Else SetLength(Result, rLength);
End;
This is a simple way I think. If you want to get the loaded DLL's full name, use
FMODULEENTRY32.hModule with GetModuleFileNameW function.

How to install files for each user, including future new users, in Inno Setup?

I have an installer which needs to distribute some default files for the user to modify. Each Windows user profile needs to have its own copy of these (writable) files, including when a new user is created in Windows in the future.
I already know how to distribute to the current user's profile, but have no idea about all user profiles, especially future users. I've seen how some software can automatically include files in a new Windows user's profile.
How can I make Inno Setup distribute files in such a manner?
For all existing accounts, see:
Inno Setup Create individual shortcuts on all desktops of all users
For future accounts: Whatever is in the Default User profile gets automatically copied to all newly created profiles.
So if you want to add a file to all new users' "documents" folder, add it to the Documents folder of the Default User profile. What typically is:
C:\Users\Default\Documents
To retrieve the correct path, use SHGetFolderPath with nFolder argument set to the path you are after (e.g. CSIDL_PERSONAL for "documents" folder) and the hToken argument set to -1 (default user profile).
[Files]
Source: "default.txt"; DestDir: "{code:GetDefaultUserDocumentsPath}"
[Code]
const
CSIDL_PERSONAL = $0005;
SHGFP_TYPE_CURRENT = 0;
MAX_PATH = 260;
S_OK = 0;
function SHGetFolderPath(
hwnd: HWND; csidl: Integer; hToken: THandle; dwFlags: DWORD;
pszPath: string): HResult;
external 'SHGetFolderPathW#shell32.dll stdcall';
function GetDefaultUserDocumentsPath(Param: string): string;
var
I: Integer;
begin
SetLength(Result, MAX_PATH);
if SHGetFolderPath(0, CSIDL_PERSONAL, -1, SHGFP_TYPE_CURRENT, Result) <> S_OK then
begin
Log('Failed to resolve path to default user profile documents folder');
end
else
begin
{ Look for NUL character and adjust the length accordingly }
SetLength(Result, Pos(#0, Result) - 1);
Log(Format('Resolved path to default user profile documents folder: %s', [Result]));
end;
end;
(The code is for Unicode version of Inno Setup).

Is it possible to filter/require installation path to be ASCII in InnoSetup?

So I wonder if it is possible to allow user to enter only ASCII as installation path? (warn him and make input path again) (problem is application we install is old and can not work with Cyrillic paths so we need to restrict user on installation stage)
To restrict the user input for the application directory for the Basic Latin character set you may use the following code. The code only checks if any char of the selected directory name doesn't exceed the Basic Latin character set range. If that happens, an error message is shown and the user is forced to stay on the directory selection page. The remaining folder name validation (based on the file system naming conventions) is left on Inno Setup internals, as it already was:
[Setup]
AppName=My Program
AppVersion=1.5
DefaultDirName={pf}\My Program
[Code]
function IsCharValid(Value: Char): Boolean;
begin
Result := Ord(Value) <= $007F;
end;
function IsDirNameValid(const Value: string): Boolean;
var
I: Integer;
begin
Result := False;
for I := 1 to Length(Value) do
if not IsCharValid(Value[I]) then
Exit;
Result := True;
end;
function NextButtonClick(CurPageID: Integer): Boolean;
begin
Result := True;
if (CurPageID = wpSelectDir) and
not IsDirNameValid(WizardForm.DirEdit.Text) then
begin
Result := False;
MsgBox('There is an invalid char in the selected directory name. ' +
'Directory path may contain only chars that are valid for the ' +
'file system naming conventions and only in the range of the ' +
'Basic Latin character set.', mbError, MB_OK);
end;
end;

Wow64DisableWow64FsRedirection fileExits

a file is located in C:\program files (x86)\my app\myexe.exe
FileExists('C:\program files (x86)\my app\myexe.exe') returns true;
FileExists('C:\program files\my app\myexe.exe') returns false;
in both cases, if I use Wow64DisableWow64FsRedirection or not.
Why ? Thanks
File system redirection is only there for the %windir%\system32 directory. The description of the File System Redirector seems to make this obvious.
Note the comment in the page
Applications should use the SHGetSpecialFolderPath function to determine the %ProgramFiles% directory name.
Edit Turns out that the FOLDERID_ProgramFilesx64 does not work on 32bit applications running on 64bit windows. In this case, you can use the environment variable %ProgramW6432% instead. Note that this variable is only available on Windows 7 and later for 32bit applications.
The following delphi snippet allows accessing the variable:
function GetEnvironmentString(aString : string) : string;
var
dest : string;
retSize : integer;
begin
SetLength(dest, MAX_PATH);
retSize := ExpandEnvironmentStrings(pchar(aString), pchar(dest), MAX_PATH);
if retSize > 0 then
SetLength(dest, retSize - 1);
result := dest;
end;
Called as:
GetEnvironmentString('%ProgramW6432%');
IF you're on a 64bit version of windows, then a 32bit application cannot use FOLDERID_ProgramFilesX64 to explicitly get the 64bit location of Program Files, but can use the environment variable expansion instead. On a 32bit version of windows, this location is invalid, and will not get you a value. You need to check the bitness of the system before attempting to access this variable.
You can use the function IsWow64Process to determine this. The following snippet should allow you to check this:
function IsWow64: Boolean;
type
TIsWow64Process = function(Handle: Windows.THandle; var Res: Windows.BOOL): Windows.BOOL; stdcall;
var
IsWow64Result: Windows.BOOL;
IsWow64Process: TIsWow64Process;
begin
// Try to load required function from kernel32
IsWow64Process := Windows.GetProcAddress(Windows.GetModuleHandle('kernel32.dll'), 'IsWow64Process');
if Assigned(IsWow64Process) then
begin
// Function is implemented: call it
if not IsWow64Process(Windows.GetCurrentProcess, IsWow64Result) then
raise SysUtils.Exception.Create('IsWow64: bad process handle');
// Return result of function
Result := IsWow64Result;
end
else
// Function not implemented: can't be running on Wow64
Result := False;
end;
In summary: FOLDERID_ProgramFiles gives you the 32/64 bit variant when accessed from a 32/64 bit program, FOLDERID_ProgramFilesX64 gives you the 64bit version explicitly on a 64-bit application, and FOLDERID_ProgramFilesX86 gives you the 32bit variant explicitly. You can use the environment variable expansion to get the 64bit value on a 32bit application

delphi - calculate directory size API?

Anyone knows other way to get a directoy's size, than calculate it's size by counting file with file? I'm interested on some win32 API function. I've google it about this, but i didn't found relevant information so i'm asking here.
PS: I know how to calculate a directory size by using findfirst and findnext and sum all file's size.
Getting the size of one directory is a pretty big problem, mostly because it's something you can't define. Examples of problems:
Some filesystems, including NTFS and most filesystems on Linux support the notion of "hard links". That is, the very same file may show up in multiple places. Soft links (reparse points) pose the same problem.
Windows allows mounting of file systems to directories. Example: "C:\DriveD" might be the same thing as "D:\".
Do you want the file size on disk or the actual size of the file?
Do you care about actual DIRECTORY entries? They also take up space!
What do you do with files you don't have access to? Or directories you don't have permission to browse? Do you count those?
Taking all this into account means the only possible implementation is a recursive implementation. You can write your own or hope you find a ready-written one, but the end performance would be the same.
I don't think there's an API for this. I don't think Windows knows the answer. i.e. it would have to recursively search the tree and summarize each folder, using the technique that Marcelo indicated.
If there were such an API call available, then other things would use it too and Windows would be able to immediately tell you folder sizes.
I already had a function to list files of a folder (and subfolders) and one to read the file size. So, I only wrote a small procedure that puts these two together.
ListFilesOf
function ListFilesOf(CONST aFolder, FileType: string; CONST ReturnFullPath, DigSubdirectories: Boolean): TTSL;
{ If DigSubdirectories is false, it will return only the top level files,
else it will return also the files in subdirectories of subdirectories.
If FullPath is true the returned files will have full path.
FileType can be something like '*.*' or '*.exe;*.bin'
Will show also the Hidden/System files.
Source Marco Cantu Delphi 2010 HandBook
// Works with UNC paths}
VAR
i: Integer;
s: string;
SubFolders, filesList: TStringDynArray;
MaskArray: TStringDynArray;
Predicate: TDirectory.TFilterPredicate;
procedure ListFiles(CONST aFolder: string);
VAR strFile: string;
begin
Predicate:=
function(const Path: string; const SearchRec: TSearchRec): Boolean
VAR Mask: string;
begin
for Mask in MaskArray DO
if System.Masks.MatchesMask(SearchRec.Name, Mask)
then EXIT(TRUE);
EXIT(FALSE);
end;
filesList:= TDirectory.GetFiles (aFolder, Predicate);
for strFile in filesList DO
if strFile<> '' { Bug undeva: imi intoarce doua intrari empty ('') }
then Result.Add(strFile);
end;
begin
{ I need this in order to prevent the EPathTooLongException (reported by some users) }
if aFolder.Length >= MAXPATH then
begin
MesajError('Path is longer than '+ IntToStr(MAXPATH)+ ' characters!');
EXIT(NIL);
end;
if NOT System.IOUtils.TDirectory.Exists (aFolder)
then RAISE Exception.Create('Folder does not exist! '+ CRLF+ aFolder);
Result:= TTSL.Create;
{ Split FileType in subcomponents }
MaskArray:= System.StrUtils.SplitString(FileType, ';');
{ Search the parent folder }
ListFiles(aFolder);
{ Search in all subfolders }
if DigSubdirectories then
begin
SubFolders:= TDirectory.GetDirectories(aFolder, TSearchOption.soAllDirectories, NIL);
for s in SubFolders DO
if cIO.DirectoryExists(s) { This solves the problem caused by broken 'Symbolic Link' folders }
then ListFiles(s);
end;
{ Remove full path }
if NOT ReturnFullPath then
for i:= 0 to Result.Count-1 DO
Result[i]:= TPath.GetFileName(Result[i]);
end;
GetFileSize
{ Works with >4GB files
Source: http://stackoverflow.com/questions/1642220/getting-size-of-a-file-in-delphi-2010-or-later }
function GetFileSize(const aFilename: String): Int64;
VAR
info: TWin32FileAttributeData;
begin
if GetFileAttributesEx(PWideChar(aFileName), GetFileExInfoStandard, #info)
then Result:= Int64(info.nFileSizeLow) or Int64(info.nFileSizeHigh shl 32)
else Result:= -1;
end;
Finally:
function GetFolderSize(aFolder: string; FileType: string= '*.*'; DigSubdirectories: Boolean= TRUE): Int64;
VAR
i: Integer;
TSL: TTSL;
begin
Result:= 0;
TSL:= ListFilesOf(aFolder, FileType, TRUE, DigSubdirectories);
TRY
for i:= 0 to TSL.Count-1 DO
Result:= Result+ GetFileSize(TSL[i]);
FINALLY
FreeAndNil(TSL);
END;
end;
Note that:
1. You can only count the size of some file types in a folder. For example in a folder containing BMP and JPEG files, if you want/need, you can only obtain the folder size only for BMP files.
2. Multiple filetypes are supported, like this: '.bmp;.png'.
3. You can choose if you want to read or not rea the size of the sub-folders.
Further improvements: You can massively reduce the size of the code by eliminating the GetFolderSize and moving the GetFileSize directly into ListFilesOf.
Guaranteed to work on Delphi XE7.
If you already know how to do another level, just use recursion to get every level. As your function traverses the current directory, if it comes across subdirectory, call the function recursively to get the size of that subdirectory.
You can use FindFile component. It will recursive the directories for you.
http://www.delphiarea.com/products/delphi-components/findfile/
Many of the previous examples of recurring find first and find next implementations do not consider the larger file capacities. Those examples do not produce correct results. A recursion routine that accommodates larger file capacities is available.
{*-----------------------------------------------------------------------------
Procedure: GetDirSize
Author: archman
Date: 21-May-2015
#Param dir: string; subdir: Boolean
#Return Int64
-----------------------------------------------------------------------------}
function TBCSDirSizeC.GetDirSize(dir: string; subdir: Boolean): Int64;
var
rec: TSearchRec;
found: Integer;
begin
Result := 0;
if dir[Length(dir)] <> '\' then
dir := dir + '\';
found := FindFirst(dir + '*.*', faAnyFile, rec);
while found = 0 do
begin
Inc(Result, rec.Size);
if (rec.Attr and faDirectory > 0) and (rec.Name[1] <> '.') and
(subdir = True) then
Inc(Result, GetDirSize(dir + rec.Name, True));
found := FindNext(rec);
end;
System.SysUtils.FindClose(rec);
end;
Please visit the link below for a complete explanation of the process.
http://bcsjava.com/blg/wordpress/2015/06/05/bcs-directory-size-delphi-xe8/

Resources