Matt asks:
My question is - is there a simple way to check for nameconflict on cffile action=rename like there ins on cffile action=upload. I have an application where I want to rename whatever they upload to match their order number. It's fine if they just upload one image, but not cool if there is more than one. I'm just checking to see if there is an easier way than some kind of fileexists, then add a 1 to the filename before the rename.
Unfortunately there isn't a simpler way, not that I'm aware of. Your solution is one I used myself very recently. Here is a live working example of code that is run after I used cffile/upload and did security checks. That portion is done and now I just need to copy it to the storage directory.
<cflock type="exclusive" name="#variables.lockname#" timeout="30">
<cfif not directoryExists(folder)>
<cflog file="picard" text="created a file store for #folder#">
<cfdirectory action="create" directory="#folder#">
</cfif>
<!--- before we move our file out of ram and onto disk, need to look for files w/ same name. If so,
we prepend N_ in front, going up forever basically --->
<cfif fileExists(folder & "/" & getFileFromPath(newFile))>
<cfset var keepGoing = true>
<cfset var counter = 1>
<cfloop condition="keepGoing">
<cfset uniqueFileName = folder & "/" & counter & "_" & getFileFromPath(newFile)>
<cfif fileExists(uniqueFileName)>
<cfset counter++>
<cfelse>
<cfset dest = uniqueFileName>
<cfset keepGoing = false>
</cfif>
</cfloop>
<cfelse>
<cfset dest = folder & "/" & getFileFromPath(newFile)>
</cfif>
<cffile action="move" source="#newFile#" destination="#dest#">
</cflock>
So a few notes on this code block. It's written for ColdFusion 9 which is why you use var keywords littered throughout. To get this to work in a CF8 CFC you would simply ensure you do the vars at the beginning.
The lock is critical here. In this case, all file moves are done via one FileService component. So the exclusive lock here ensures that only one move is done at a time. I did my best to minimize the size of the lock. It's only done after the upload processing and security checks. This is also an big hole in the logic. It assumes only ColdFusion has access to the folder. Obviously ColdFusion has no way to really lock a folder by itself. Some other process could drop a file in the folder at the same time.
I assume the main logic of this is pretty simple. I use a conditional loop that increments a counter which is prepended to the file name. In theory this should run pretty darn quickly. Outside of this block I've used logic to create date based folders so that ensures I don't end up with one folder that contains a few thousand files.
Lastly - there is another nice way to handle this as well. Use UUID file names. You can grab the extension of the original file and simply use the UUID as the filename. So foo.jpg becomes #createUUID()#.jpg. This will create an ugly, but unique file name, in far fewer lines of code. I'd use this if you don't care about the original file name. A good example of this would be for things like images.
Archived Comments
I like the UUID approach as well. It is a lot less convoluted than the alternative (which I have used). If you are also storing the file names in a database, you could always add an extra column to preserve the "pretty" name for display or download. Then you get the best of both worlds.
-Leigh
Hey Ray! I know you are a COldfusion/Jquery Pro but just wondering if yo have any expertise in the Facebook API. My question is how you get the url parameters using coldfusion, such as apps.facebook.com/rayobi-ka... =1 . How do i capture the value of the "jedilevel " parameter, <cfoutput>#URL.jedilevel# produces an error. Thanks Master Ray!
I have worked w/ the FB API, but I'm a bit rusty. However, any URL variable should exist - period. Maybe it really isn't what you think it is though. Have you tried doing <cflog file="ray" text="#cgi.query_string#">? Obviously you don't need to use ray as a filename, but you get my point. You can also log #structkeylist(url)# instead. You may also want to log cgi.path_info.
Hello Ray,
What are you using for the lock name?
I would have assumed that to minimise the locking you would set the lock name to be name="#folder##filename#". That way it prevent collision with the same file, but allows multiple renames on different files at the same time.
Is that your value for lockname?
Cheers.
The lock name is something like fileService, so it's totally blocking for all file operations. I agree that making it based on foldername would be nicer. I don't think folder/file though would make sense, because 2 requests could possibly end up conflicting.
I remember an interesting thread on the HOF a while back. Someone mentioned using full file paths (hashed) for the lock name. But with a double lock: one on the origin file and one on the destination. So it was more granular, but still covered both files.
What concerns me is thread A trying to "put" to foo.cfm and thread B trying to put to 1_foo.cfm. They would have different lock names, but potentially end up trying to use the same file.
Scratch that last comment. My mind is somewhere else. I was thinking of something totally different..
Good point about name conflicts with files already using the special naming convention.
To get the granularity back into the locking, I would parse the filename to remove the special naming convention and again lock based on folder and stripped filename.
local.lockname = local.folder & ReReplace(local.filename, "^[0-9]*_", "");
Which would make the lock for "foo.cfm" and "1_foo.cfm" the same. Thereby reducing the scope of the lock to the safest minimum.
Here's a little different spin on generating a unique file name given an order number, file extension, and a directory where orderNum.jpg, orderNum_#.jpg, or any img or file for that matter which may or may not exist.
<!--- Known variables --->
<cfset ordNum = "00101"> <!--- example order num --->
<cfset ext = "jpg"> <!--- file extension --->
<cfset fileDir = "YOUR ABSOLUTE FILE PATH HERE">
<!--- use java's "java.io.File" to get a list of file names from the directory, no need to retrieve a query object from cfdirectory for just reading file names --->
<cfset dir = createObject("Java", "java.io.File").init(fileDir)>
<!--- get a comma delim list of file names --->
<cfset fileList = arrayToList(dir.list())>
<cfset count = listLen(fileList) - listLen(rereplacenocase("|"&replace(fileList,",","|,|","all")&"|","\|"&ordNum&"\."&ext&"\||\|"&ordNum&"\_[0-9]+\."&ext&"\|","","all"))>
<!--- default file name --->
<cfset newFileName = ordNum&"."&ext>
<cfif count>
<!--- if file name exists already, assign an incremented name, ie: fileName_#.ext --->
<cfset newFileName = ordNum&"_"&count&"."&ext>
</cfif>
<cfdump var="#newFileName#">
This approach avoids looping, looping over conditions, and fileExists() in exhange for a regex and a little math on the directory list. On the flip side there's really no one liner solution for cffile: action="rename" on naming conflicts.
Thank you @Adam for posting your version, I was able to take your version and fork it again to what we are using now:
<cffunction name="UnqiueFileName" access="private" returntype="string" output="false">
<cfargument name="fFileName" type="string" required="true">
<cfargument name="fFileDestination" type="string" required="true">
<cfset var FullDirPath=arguments.fFileDestination & Instance.AppShared.AppServerInfo.AppFilePathSeparator>
<cfset var base = "#dateformat(now(),'yyyymmdd')##timeformat(now(),'hhmmss')#">
<!--- if the filename is not found use the name--->
<cfset newFileName = arguments.fFileName>
<!--- use java's "java.io.File" to get a list of file names from the directory, no need to retrieve a query object from cfdirectory for just reading file names --->
<cfset dir = createObject("Java", "java.io.File").init(FullDirPath)>
<!--- get a comma delim list of file names --->
<cfset fileList = arrayToList(dir.list())>
<cfif ListLen(fileList) GT 0>
<cfif ListContainsNoCase(fileList,arguments.fFileName,",") GT 0>
<cfset newFileName = base & "_" & arguments.fFileName>
</cfif>
</cfif>
<cfreturn newFileName/>
</cffunction>
With cffile action="upload" nameconflict="overwrite", #cffile.serverfile# stores the value of the file after the nameconflict has been processed.
With cffile action="rename" nameconflict="overwrite", there is no cffile variable.
How do you find the value of the file name after the nameconflict has been processed?
I'm confused - if you are renaming, aren't you specifying both the original and new name?
Yes, but if there's a conflict with the name you specify, it creates another name that you have no way of retrieving.
Upload and rename work differently. Upload conflict stores a name conflict in a variable, rename doesn't.
I just appended a long timestamp to end of the file.
Thank you,
Dale
Personally I'd not use nameconflict="overwrite". It sounds like the conflict is not expected. I'd want an error thrown.