This is for a file sharing website. In order to make sure a “passcode”, which is unique to each file, is truely unique, I’m trying this:
$genpasscode = mysql_real_escape_string(sha1($row['name'].time())); //Make passcode out of time + filename.
$i = 0;
while ($i < 1) //Create new passcode in loop until $i = 1;
{
$query = "SELECT * FROM files WHERE passcode='".$genpasscode."'";
$res = mysql_query($query);
if (mysql_num_rows($res) == 0) // Passcode doesn't exist yet? Stop making a new one!
{
$i = 1;
}
else // Passcode exists? Make a new one!
{
$genpasscode = mysql_real_escape_string(sha1($row['name'].time()));
}
}
This really only prevents a double passcode if two users upload a file with the same name at the exact same time, but hey better safe than sorry right? My question is; does this work the way I intend it to? I have no way to reliably (read: easily) test it because even one second off would generate a unique passcode anyway.
UPDATE:
Lee suggest I do it like this:
do {
$query = "INSERT IGNORE INTO files
(filename, passcode) values ('whatever', SHA1(NOW()))";
$res = mysql_query($query);
} while( $res && (0 == mysql_affected_rows()) )
[Edit: I updated above example to include two crucial fixes. See my answer below for details. -@Lee]
But I’m afraid it will update someone else’s row. Which wouldn’t be a problem if filename and passcode were the only fields in the database. But in addition to that there’s also checks for mime type etc. so I was thinking of this:
//Add file
$sql = "INSERT INTO files (name) VALUES ('".$str."')";
mysql_query($sql) or die(mysql_error());
//Add passcode to last inserted file
$lastid = mysql_insert_id();
$genpasscode = mysql_real_escape_string(sha1($str.$lastid.time())); //Make passcode out of time + id + filename.
$sql = "UPDATE files SET passcode='".$genpasscode."' WHERE id=$lastid";
mysql_query($sql) or die(mysql_error());
Would that be the best solution? The last-inserted-id field is always unique so the passcode should be too. Any thoughts?
UPDATE2: Apperenatly IGNORE does not replace a row if it already exists. This was a misunderstanding on my part, so that’s probably the best way to go!
Strictly speaking, your test for uniqueness won’t guarantee uniqueness under a concurrent load. The problem is that you check for uniqueness prior to (and separately from) the place where you insert a row to “claim” your newly generated passcode. Another process could be doing the same thing, at the same time. Here’s how that goes…
Two processes generate the exact same passcode. They each begin by checking for uniqueness. Since neither process has (yet) inserted a row to the table, both processes will find no matching passcode in database, and so both processes will assume that the code is unique. Now as the processes each continue their work, eventually they will both insert a row to the
filestable using the generated code — and thus you get a duplicate.To get around this, you must perform the check, and do the insert in a single “atomic” operation. Following is an explanation of this approach:
If you want passcode to be unique, you should define the column in your database as
UNIQUE. This will ensure uniqueness (even if your php code does not) by refusing to insert a row that would cause a duplicate passcode.Now, use mysql’s
SHA1()andNOW()to generate your passcode as part of the insert statement. Combine this withINSERT IGNORE ...(docs), and loop until a row is successfully inserted:Note: The above example was edited to account for the excellent observations posted by @martinstoeckli in the comments section. The following changes were made:
mysql_num_rows()(docs) tomysql_affected_rows()(docs) — num_rows doesn’t apply to inserts. Also removed the argument tomysql_affected_rows(), as this function operates on the connection level, not the result level (and in any case, the result of an insert is boolean, not a resource number).IGNORE, andmysql_affected_rows(), and testing$resseparately for errors) allows us to distinguish these “real database errors” from the unique constraint violation (which is a completely valid non-error condition in this section of logic).If you need to get the passcode after it has been generated, just select the record again:
Edit: changed above example to use the mysql function
LAST_INSERT_ID(), rather than PHP’s function. This is a more efficient way to accomplish the same thing, and the resulting code is cleaner, clearer, and less cluttered.