VB6 - How to detect a file is finished copying from an external source - vb6

My software (written in VB6) needs to import csv files that can be large. Users are using copy/paste to place the files in the input folder.
How can I be sure the files I want to read are fully copied before processing them?
Things I tried :
Compare GetFileSizeString over a span of seconds : doesn't work, I get the final value even if the file has just begun to copy.
FileSystemObject.DateLastModified : same
FileSystemObject.DateLastAccessed : same
FileLen : same
FileDateTime : same
EDIT - Added Samba/Linux Info (from comments):
I'm having a hard time dealing with is from Samba/Linux. I don't know why, but when the file is copied by Samba, the read only attribute doesn't matter.

I've used this method which uses the API to test for exclusive access. I've never tried it on a platform other than Windows so, I guess result may vary. I use this in a module of frequently used methods, but I believe I have included the API calls, types, and constants used.
Private Const ERROR_SHARING_VIOLATION = 32&
Private Const GENERIC_WRITE = &H40000000
Private Const INVALID_HANDLE_VALUE = -1
Private Const OPEN_EXISTING = 3
Private Type SECURITY_ATTRIBUTES
nLength As Long
lpSecurityDescriptor As Long
bInheritHandle As Long
End Type
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Declare Function CreateFile Lib "kernel32" Alias "CreateFileA" (ByVal lpFileName As String, ByVal dwDesiredAccess As Long, ByVal dwShareMode As Long, lpSecurityAttributes As SECURITY_ATTRIBUTES, ByVal dwCreationDisposition As Long, ByVal dwFlagsAndAttributes As Long, ByVal hTemplateFile As Any) As Long
Public Function CanOpenExclusive(ByVal vFileName As String) As Boolean
Dim lngResult As Long
Dim udtSA As SECURITY_ATTRIBUTES
On Error GoTo errCanOpenExclusive
If Len(vFileName) > 0 Then
udtSA.nLength = Len(udtSA)
udtSA.bInheritHandle = 1&
udtSA.lpSecurityDescriptor = 0&
lngResult = CreateFile(vFileName, GENERIC_WRITE, 0&, udtSA, OPEN_EXISTING, 0&, 0&)
If lngResult <> INVALID_HANDLE_VALUE Then
Call CloseHandle(lngResult)
CanOpenExclusive = True
Else
Select Case Err.LastDllError 'some errors may indicate the file exists, but there was an error opening it
Case Is = ERROR_SHARING_VIOLATION
CanOpenExclusive = False
Case Else
GoTo errCanOpenExclusive
End Select
End If
End If
Exit Function
errCanOpenExclusive:
Err.Raise Err.Number, Err.Source & ":CanOpenExclusive", Err.Description
End Function

I would use the FileDateTime function to work with to calculate when a date/time value indicating the date and time that a file was created or last modified.
You can read up more on the file system usage HERE.
In the syntax above, pathname is a string expression specifying a valid path (it may optionally include the drive); drive is a string expression specifying a drive letter; and filespec, oldfilespec, and newfilespec are string expressions that specify a file (they may optionally include the drive and path).
Following is a set of functions that can be used with files (all are functions except SetAttr, which is a statement):
Function
Description
Syntax
FileDateTime
Returns a date/time value indicating the date and time that a file was created or last modified.
FileDateTime(filespec)
GetAttr
Returns an integer representing the attributes of a file, directory, or folder
GetAttr(filespec)
SetAttr
Statement that lets you specify the attributes for a file
SetAttr(filespec, attributes)
CurDir$ (or CurDir)
Returns a string that indicates the current path for a specified disk drive. In the syntax on the right, drivename is a string expression that specifies a valid disk drive designation
CurDir$(drivename)
Dir$ (or DIr)
Returns a string that indicates a file or directory matching specified conditions
Dir$(filespec [,attributes])
FileLen
Returns a Long specifying the length of a file in bytes. If the specified file is open when the FileLen function is called, the value returned represents the size of the file immediately before it was opened.
FileLen(pathname)
LOF
Returns a Long representing the size, in bytes, of a file opened using the Open statement.
FileLen(filenumber)

Related

How to find correct values for HWND window handles?

WinRestore,% hwnd([1])
i have found in many programming language the use of hwnd. after searching on google it comes out to be handle. I didnt got more information on this. how programmer knows the value to put in, eg.
Const LB_GETTEXTLEN = &H18A
Const LB_GETTEXT = &H189
Const LB_GETCOUNT = &H18B
&h18a how he known, how will he use this?
this is the example program
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As Long
Const LB_GETTEXTLEN = &H18A
Const LB_GETTEXT = &H189
Const LB_GETCOUNT = &H18B
Private Function GetListItems(ByVal hList As Long) As Variant
Dim i As Long, nCount As Long, lItemLength As Long
Dim sItem() As String
nCount = SendMessage(hList, LB_GETCOUNT, 0, ByVal 0&)
For i = 0 To nCount - 1
lItemLength = SendMessage(hList, LB_GETTEXTLEN, i, ByVal 0&)
ReDim Preserve sItem(i)
sItem(i) = String(lItemLength, 0)
Call SendMessage(hList, LB_GETTEXT, i, ByVal sItem(i))
Next i
GetListItems = sItem
End Function
there are many such examples in all different languages but concept will be the same. so i want to learn it. what does it mean and how to use it.
another example from ahk
Gui,2:+hwndhwnd
hwnd(2,hwnd)
Those are all window messages that you can find information about on the MSDN Documentation by googling them. See below links:
LB_GETTEXTLEN
LB_GETTEXT
LB_GETCOUNT
You can find them and other related messages by checking the documentation for the native List Box control.
As for the numbers they're hexadecimal numbers which are (usually) mentioned in the documentation. But since these aren't you'll have to google them and check other websites/forums, or find their values on your own by experimenting with them in C or C++ .
In VB hexadecimal numbers are represented by prepending the number with &H, whereas in C, C++, C# or alike they're prepended with 0x.
In a forms editor each window/control has a hwnd property. For windows not created by your forms package you use the API calls FindWindow (easiest but not reliable) or EnumWindows. Also GetForegroundWindow and GetDesktopWindow.
To find out the value of constants you download the C header files as part of the Windows SDK https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk. It also has the documentation for all these API calls. This is online documentation listing all the windows' functions https://msdn.microsoft.com/en-us/library/windows/desktop/ms633505(v=vs.85).aspx.

ShellExecuteEx print in VBA7 fails with access denied

We have VBA code in a Word macro that is used to download one or more documents, then print them using the Windows function ShellExecuteEx. The code runs successfully in Word versions 97, 2000, 2003, 2007 and 2010 (32-bit) on Windows 2000, XP and 7 (32-bit and 64-bit).
But the call to ShellExecuteEx fails in 64-bit Word 2010 and 2013. We have updated the declarations for VBA7 (64-bit) as documented on MSDN and specified in the Win32API_PtrSafe file. For example:
#If VBA7 Then
Type SHELLEXECUTEINFO
cbSize As Long
fMask As Long
hwnd As LongPtr
lpVerb As String
lpFile As String
lpParameters As String
lpDirectory As String
nShow As Long
hInstApp As LongPtr
lpIDList As LongPtr
lpClass As String
hkeyClass As LongPtr
dwHotKey As Long
hIcon As LongPtr
hProcess As LongPtr
End Type
Declare PtrSafe Function ShellExecuteEx Lib "shell32.dll" Alias "ShellExecuteExA" _
(sei As SHELLEXECUTEINFO) As Boolean
#End If
Usage is like this:
Dim bReturn As Boolean
Dim sei As SHELLEXECUTEINFO
With sei
.cbSize = Len(sei) ' size of the object
.fMask = SEE_MASK_NOCLOSEPROCESS ' indicate that we want a hProcess back
.hwnd = GetDesktopWindow() ' the window we are calling from
.lpVerb = "print" ' print the file
.lpFile = lpFile ' the file we want to print
.lpParameters = vbNullString ' no parameters because its a file
.lpDirectory = vbNullString ' use the current dir as working dir
.nShow = SW_HIDE ' state of the window to open
End With
bReturn = ShellExecuteEx(sei)
If bReturn Then
WaitForSingleObject sei.hProcess, 5000
CloseHandle sei.hProcess
DoEvents
Else
MsgBox "ShellExecuteEx failed with code: " & Err.LastDllError
End If
In 32-bit Word it works but in 64-bit Word the call to ShellExecuteEx always fails, returning 5 (SE_ERR_ACCESSDENIED). I have tried a range of flag values for fMask (including SEE_MASK_NOASYNC), tried not specifying a value for hwnd and different values for nShow, all with the same failed result.
The simpler ShellExecute function works in both 32-bit and 64-bit Word but it is too inflexible. We want to use ShellExecuteEx because it is better when printing multiple documents: it gives us the ability to wait for the printing application (Word, Adobe Reader etc.) to be ready before sending another print request. Otherwise a print request fails if the application is not ready. (I tried simply waiting for a few seconds between print requests but that is not reliable.)
Why does ShellExecute print files but ShellExecuteEx fail with access denied?
You have to use LenB instead of Len for 64 version OS.
The whole answer is here: http://www.utteraccess.com/forum/office-2010-x64-bit-qu-t1914261.html

VB6: Single-instance application across all user sessions

I have an application that needs to be a single-instance app across all user sessions on a Windows PC. My research thus far has centered around using a mutex to accomplish this, but I am having an issue that I am not sure is really an issue, this is really a best-practice question I believe.
Here's the code first of all:
Private Const AppVer = "Global\UNIQUENAME" ' This is not what i am using but the name is unique
Public Sub Main()
Dim mutexValue As Long
mutexValue = CreateMutex(ByVal 0&, 1, AppVer)
If (Err.LastDllError = ERROR_ALREADY_EXISTS) Then
SaveTitle$ = App.Title
App.Title = "... duplicate instance."
MsgBox "A duplicate instance of this program exists."
CloseHandle mutexValue
Exit Sub
End If
' Else keep on truckin'
Now, based on this article I believe I understand that by passing the NULL pointer to the CreateMutex function as I am above I'm basically assigning whatever security descriptor is associated with the currently logged in user.
If that means what I think it does (I may need more guidance here) that tells me that other users who log in will not be able to "see" the mutex created under the original user's session, nor will they be able to create a mutex with the same name.
Now, emperical evidence seems to back this up. I used a message box to pop the "LastDLLError" I was receiving, and when another user attempted to launch the application (while it was already running under another user account) I would receive an ERROR_ACCESS_DENIED code. I am OK with testing against this along with the ERROR_ALREADY_EXISTS code and just exiting on either/or. However, this feels sort of hackish and I'm wondering if someone can suggest an alternative. The "right" thing to do seems to be to pass the proper pointer to the CreateMutex function such that any user has the proper permissions to view any existing mutexes (mutices?), but I'm not so sure this is possible without the currently logged in user being an admin (which is unacceptible). Any assistance/guidance is greatly appreciated. Thanks in advance!
You don't need admin priveleges to set security on you own mutexes. Here is a simple demo app that basicly gives Everyone/Full control to the mutex.
Option Explicit
Private Const STANDARD_RIGHTS_REQUIRED As Long = &HF0000
Private Const SYNCHRONIZE As Long = &H100000
Private Const MUTANT_QUERY_STATE As Long = &H1
Private Const MUTANT_ALL_ACCESS As Long = (STANDARD_RIGHTS_REQUIRED Or SYNCHRONIZE Or MUTANT_QUERY_STATE)
Private Const SECURITY_DESCRIPTOR_REVISION As Long = 1
Private Const DACL_SECURITY_INFORMATION As Long = 4
Private Declare Function CreateMutex Lib "kernel32" Alias "CreateMutexA" (lpMutexAttributes As Any, ByVal bInitialOwner As Long, ByVal lpName As String) As Long
Private Declare Function OpenMutex Lib "kernel32" Alias "OpenMutexA" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal lpName As String) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Declare Function InitializeSecurityDescriptor Lib "advapi32.dll" (pSecurityDescriptor As Any, ByVal dwRevision As Long) As Long
Private Declare Function SetSecurityDescriptorDacl Lib "advapi32.dll" (pSecurityDescriptor As Any, ByVal bDaclPresent As Long, pDacl As Any, ByVal bDaclDefaulted As Long) As Long
Private Declare Function SetKernelObjectSecurity Lib "advapi32.dll" (ByVal Handle As Long, ByVal SecurityInformation As Long, pSecurityDescriptor As SECURITY_DESCRIPTOR) As Long
Private Type SECURITY_DESCRIPTOR
Revision As Byte
Sbz1 As Byte
Control As Long
Owner As Long
Group As Long
pSacl As Long
pDacl As Long
End Type
Private Const MUTEX_NAME As String = "Global\20b70e57-1c2e-4de9-99e5-20f3961e6812"
Private m_hCurrentMutex As Long
Private Sub Form_Load()
Dim hMutex As Long
Dim uSec As SECURITY_DESCRIPTOR
hMutex = OpenMutex(MUTANT_ALL_ACCESS, 0, MUTEX_NAME)
If hMutex <> 0 Then
Call CloseHandle(hMutex)
MsgBox "Already running", vbExclamation
Unload Me
Exit Sub
End If
m_hCurrentMutex = CreateMutex(ByVal 0&, 1, MUTEX_NAME)
Call InitializeSecurityDescriptor(uSec, SECURITY_DESCRIPTOR_REVISION)
Call SetSecurityDescriptorDacl(uSec, 1, ByVal 0, 0)
Call SetKernelObjectSecurity(m_hCurrentMutex, DACL_SECURITY_INFORMATION, uSec)
End Sub
Private Sub Form_Unload(Cancel As Integer)
If m_hCurrentMutex <> 0 Then
Call CloseHandle(m_hCurrentMutex)
m_hCurrentMutex = 0
End If
End Sub
I was looking for a similar solution in VB6 late last year. At the time I was unable to find any examples of VB6 apps communicating across the user boundary, so I had to write my own.
See: Interprocess Communication via Semaphores
You can use the class to create and check for a global semaphore which will tell you if your app is already running under any user. I didn't look at the Mutex APIs but their usage is very similar. The GetSecurityDescriptor function is what you'll want to transpose if you've already got some Mutex code written.
I think your instincts are exactly right. I don't know any reason why it wouldn't be safe to infer from ERROR_ACCESS_DENIED that some other process has the mutex, so effectively it's the same as ERROR_ALREADY_EXISTS (in this context.) But at the same time, it doesn't feel quite right.
As you suggest, setting a proper security descriptor is indeed the right way to do it. MSDN says that granting MUTEX_ALL_ACCESS privileges increases the risk that the user will have to be an admin, and I think you do need MUTEX_ALL_ACCESS. But in my experience it works fine for non-admins.
Your question intrigued me enough do a quick test. That means I have some source code, and so here it is:
int wmain(int argc, wchar_t* argv[])
{
ACL *existing_dacl = NULL;
ACL *new_dacl = NULL;
PSECURITY_DESCRIPTOR security_descriptor = NULL;
bool owner = false;
HANDLE mutex = CreateMutex(NULL,FALSE,L"Global\\blah");
if(mutex == NULL)
wprintf(L"CreateMutex failed: 0x%08x\r\n",GetLastError());
if(GetLastError() == ERROR_ALREADY_EXISTS)
wprintf(L"Got handle to existing mutex\r\n");
else
{
wprintf(L"Created new mutex\r\n");
owner = true;
}
if(owner)
{
// Get the DACL on the mutex
HRESULT hr = GetSecurityInfo(mutex,SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,NULL,NULL,
&existing_dacl,NULL,
&security_descriptor);
if(hr != S_OK)
wprintf(L"GetSecurityInfo failed: 0x%08x\r\n",hr);
// Add an ACE to the ACL
EXPLICIT_ACCESSW ace;
memset(&ace,0,sizeof(ace));
ace.grfAccessPermissions = MUTEX_ALL_ACCESS;
ace.grfAccessMode = GRANT_ACCESS;
ace.grfInheritance = NO_INHERITANCE;
ace.Trustee.pMultipleTrustee = NULL;
ace.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
ace.Trustee.TrusteeForm = TRUSTEE_IS_NAME;
ace.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
ace.Trustee.ptstrName = L"EVERYONE";
hr = SetEntriesInAcl(1,&ace,existing_dacl,&new_dacl);
if(hr != S_OK)
wprintf(L"SetEntriesInAcl failed: 0x%08x\r\n",hr);
// Set the modified DACL on the mutex
hr = SetSecurityInfo(mutex,SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,NULL,NULL,new_dacl,NULL);
if(hr != S_OK)
wprintf(L"SetSecurityInfo failed: 0x%08x\r\n",hr);
else
wprintf(L"Changed ACL\r\n");
LocalFree(existing_dacl);
LocalFree(new_dacl);
LocalFree(security_descriptor);
}
wprintf(L"Press any key...");
_getch();
CloseHandle(mutex);
return 0;
}

Why does my ReadFile call fail if it is not called within the same function as CreatePipe?

I'm having trouble using pipes in a larger application and so I created a minimal test application to investigate the problem.
I'm creating a pipe:
Dim sa As SECURITY_ATTRIBUTES
Dim R As Long
sa.nLength = Len(sa)
sa.bInheritHandle = 1
R = CreatePipe(hRead, hWrite, sa, 0) //hRead declared globally
Debug.Print "CreatePipe: " & R
and then I'm reading from it:
Const BufSize As Long = 1024
Dim Buffer(BufSize) As Byte
Dim lBytesRead As Long
Dim R As Long
R = ReadFile(hRead, Buffer(0), BufSize, lBytesRead, 0)
Debug.Print "ReadFile: " & R
Debug.Print Err.LastDllError
Now as far as I understand the ReadFile call should block, because nobody has written any data into the pipe.
The problem: That only happens iff I put the code right after the CreatePipe code. As soon as I put it into a separate function, it fails with the last error being ERROR_INVALID_HANDLE. (I confirmed that the value of hRead does not change)
I have absolutely no idea what's causing this kind of behaviour.
I found the solution myself. It was a rather stupid beginner's mistake, but I found some users having the same problem and since the error symptoms where not at all pointing in the right direction, I'll post my solution.
First I did further refactorings to dumb down the code even more. After making each and every variable global, I finally got a "invalid calling convention" error on the ReadFile call.
To make a long story short, the import declaration and the actual call of ReadFile were wrong for the last parameter (the OVERLAPPED parameter)
This is what I did:
Declare Function ReadFile ..., lpOverlapped As Any) As Long
Call ReadFile(..., 0)
Correct would be any one of these:
Declare Function ReadFile ..., lpOverlapped As Any) As Long
Call ReadFile(..., ByVal 0)
Declare Function ReadFile ..., ByVal lpOverlapped As Long) As Long
Call ReadFile(..., 0)
Declare Function ReadFile ..., lpOverlapped As OVERLAPPED) As Long
Call ReadFile(..., myVariableOfTypeOverlapped)

VB6, File Doesn't Exist, How do I handle Gracefully?

I am testing an application that checks if a file exists across a network. In my testing, I am purposefully pulling the network plug so the file will not be found. The problem is this causes my app to go unresponsive for at least 15 seconds. I have used both the FileExists() and GetAttr() functions in VB6. Does anyone know how to fix this problem? (No, I can't stop using VB6)
Thanks,
Charlie
Unfortunately, VB doesn't make this easy, but luckily the Win32 API does, and it's quite simple to call Win32 functions from within VB.
For the LAN/WAN, you can use a combination of the following Win32 API calls to tell you whether the remote connection exists without having to deal with a network time-out:
Private Declare Function WNetGetConnection Lib "mpr.dll" Alias _
"WNetGetConnectionA" (ByVal lpszLocalName As String, _
ByVal lpszRemoteName As String, ByRef cbRemoteName As Long) As Long
Private Declare Function PathIsNetworkPath Lib "shlwapi.dll" Alias _
"PathIsNetworkPathA" (ByVal pszPath As String) As Long
Private Declare Function PathIsUNC Lib "shlwapi.dll" Alias "PathIsUNCA" _
(ByVal pszPath As String) As Long
For the Internet, you can use the Win32 API call:
Private Declare Function InternetGetConnectedState Lib "wininet.dll" _
(ByRef lpdwFlags As Long, ByVal dwReserved As Long) As Long
Const INTERNET_CONNECTION_MODEM = 1
Const INTERNET_CONNECTION_LAN = 2
Const INTERNET_CONNECTION_PROXY = 4
Const INTERNET_CONNECTION_MODEM_BUSY = 8
This VB site has more discussion on path oriented functions you can call in the Win32 API through VB.
use this too
Dim FlSize as long
flsize=filelen("yourfilepath")
if err.number=53 then msgbox("file not found")
if err.number=78 then msgbox("Path Does no Exist")
I'm not sure you can handle this much more gracefully - if the network is having problems it can take a while for timeouts to indicate that the problem is severe enough that things aren't working.
If VB6 supports threading (I honestly don't recall) you could spin the file open into a background thread, and have the UI allow the user to cancel it (or perform other operations if that makes sense), but that introduces a pretty significant amount of additional complexity.
VB6 has some networking functions that can test to see if the network is connected. You should be able to add in under 'References' the 'NetCon 1.0 Type Library'. This adds for you the NETCONLib. Once implemented, you should be able to test for network connectivity first, then test for the FileExists and GetAttr.
Let me know if this helps!
VB is inherently single threaded, but you can divert work to a COM component to do an asynchronous file check and flag an event when it is done. This way the UI thread stays at responsive at least. Trouble is - this is all theory, I don't know such a component.
But wait! Google just turned up this: Visual Basic 6 Asynchronous File I/O Using the .NET Framework. Does that help, maybe?
Also, they have something similar over at CodeProject: Asynchronous processing - Basics and a walkthrough with VB6/ VB.NET
this code only used for check connection (maybe can help you) for one of your problems :
Private Declare Function InternetGetConnectedState Lib "wininet.dll" (ByRef dwFlags As Long, ByVal dwReserved As Long) As Long
Private Const CONNECT_LAN As Long = &H2
Private Const CONNECT_MODEM As Long = &H1
Private Const CONNECT_PROXY As Long = &H4
Private Const CONNECT_OFFLINE As Long = &H20
Private Const CONNECT_CONFIGURED As Long = &H40
Public Function checknet() As Boolean
Dim Msg As String
If IsWebConnected(Msg) Then
checknet = True
Else
If (Msg = "LAN") Or (Msg = "Offline") Or (Msg = "Configured") Or (Msg = "Proxy") Then
checknet = False
End If
End If
End Function
Private Function IsWebConnected(Optional ByRef ConnType As String) As Boolean
Dim dwFlags As Long
Dim WebTest As Boolean
ConnType = ""
WebTest = InternetGetConnectedState(dwFlags, 0&)
Select Case WebTest
Case dwFlags And CONNECT_LAN: ConnType = "LAN"
Case dwFlags And CONNECT_MODEM: ConnType = "Modem"
Case dwFlags And CONNECT_PROXY: ConnType = "Proxy"
Case dwFlags And CONNECT_OFFLINE: ConnType = "Offline"
Case dwFlags And CONNECT_CONFIGURED: ConnType = "Configured"
Case dwFlags And CONNECT_RAS: ConnType = "Remote"
End Select
IsWebConnected = WebTest
End Function
in your event :
If checknet = False Then
...
else
...
end if
I agree with Will. Something like this is simple to handle with Script.FileSystemObject:
Dim objFSO As New FileSystemObject
If objFSO.FileExists("C:\path\to\your_file.txt") Then
' Do some stuff with the file
Else
' File isn't here...be nice to the user.
EndIf
Accessing files over a network can cause these hangs.
It's been a while, but I remember multi-threading in VB6 being relatively painful to implement. A quick solution would be to have a small .exe (perhaps also coded in VB) that can handle this. You could use DDE for inter-app communication or the ever so easy but kludgey file-based pipe, by which I mean a file that both apps will mutually read/write to handle inter-app communication. Of course, using file-based pipes, depending on the details of this scenario, may simply exaggerate the File I/O lag.
If there's a reasonable degree with which you can predict where the user will be selecting files from, you may consider preemptively caching a directory listing and reading that rather than the file directly - assuming the directory contents aren't expected to change frequently. Note: getting a directory listing over a network will cause the same lag issues as individual file I/O over a network. Keep that in mind.

Resources