9

I am using the Amazon S3 API to upload files and I am changing the name of the file each time I upload.

So for example:

Dog.png > 3Sf5f.png

Now I got the random part working as such:

function rand_string( $length ) {
            $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";  

            $size = strlen( $chars );
            for( $i = 0; $i < $length; $i++ ) {
                $str .= $chars[ rand( 0, $size - 1 ) ];
            }

            return $str;
        }   

So I set the random_string to the name parameter as such:

$params->key = rand_string(5);

Now my problem is that this wont show any extension. So the file will upload as 3Sf5f instead of 3Sf5f.png.

The variable $filename gives me the full name of the file with its extension.

If I use $params->key = rand_string(5).'${filename}'; I get:

3Sf5fDog.png

So I tried to retrieve the $filename extension and apply it. I tried more than 30 methods without any positive one.

For example I tried the $path_info(), I tried substr(strrchr($file_name,'.'),1); any many more. All of them give me either 3Sf5fDog.png or just 3Sf5f.

An example of what I tried:

// As @jcinacio pointed out. Change this to: 
//
//   $file_name = "${filename}";
//
$file_name = '${filename}'  // Is is wrong, since '..' does not evaluate 

$params->key = rand_string(5).$file_name;
=
3Sf5fDog.png

.

$file_name = substr(strrchr('${filename}', '.'), 1);

$params->key = rand_string(5).$file_name;
=
3Sf5f

.

$filename = "example.png"   // If I declare my own the filename it works.
$file_name = substr(strrchr('${filename}', '.'), 1);

$params->key = rand_string(5).$file_name;
=
3Sf5f.png

The entire class file: http://pastebin.com/QAwJphmW (there are no other files for the entire script).

What I'm I doing wrong? This is really frustrating.

inigomedina
  • 1,791
  • 14
  • 21
jQuerybeast
  • 14,130
  • 38
  • 118
  • 196
  • Any possibility of providing the actual code, that compiles? all of the examples have errors... – J.C. Inacio May 13 '12 at 19:42
  • What do you mean the actual code? The entire file? – jQuerybeast May 13 '12 at 19:43
  • For example, your samples are missing a required semicolon. – J.C. Inacio May 13 '12 at 19:45
  • I am on a phone. I was expecting something to be missing. Sorry. On my actual code there are no 'compiling' errors. – jQuerybeast May 13 '12 at 19:46
  • 1
    Notice: `$filename = "${file_name}";` and `$filename = '${file_name}';` is completely different, as the second does not evaluate $file_name as a variable! – J.C. Inacio May 13 '12 at 19:59
  • @jQuerybeast .. an therefore, the value of $filename is this literal value: ${file_name} – SteAp May 13 '12 at 20:14
  • @jcinacio I am a php beginner so I dont know whether it evalutes or not. All I know is that if I use "..", the filename doesn't print and if I use '..' it shows the entire filename with its extension. – jQuerybeast May 14 '12 at 04:51

12 Answers12

8

The variable $filename (and thus "${filename}") is NOT IN SCOPE at line 1053 of your code (line numbering based on raw paste from pastebin).

So, no matter what you do, you'll never find the extension of a variable that does not exist.


And I've finally worked out what you're doing. I presume this is an extension of PHP: Rename file before upload

Simple answer: you can't do it as you envisage.Why - the '$filename' is not parsed at the time that URL is created, but the variable is passed to Amazon S3 and handled there.

The solution

So, the only option I can think of is to have use the "successRedirect" parameter to point to another URL. That URL will receive the "bucket" and "key" as query parameters from Amazon (http://doc.s3.amazonaws.com/proposals/post.html#Dealing_with_Success). Point that to a PHP script that renames the file on Amazon S3 (copy + delete), then redirects the user to another success screen.

So,

in your code, line 34,

  1. add a fully qualified URL to a new php script file you're going to write.
  2. the php script wil get the bucket and key passed to it
  3. Create the new filename from the "key"
  4. use the function "public static function copyObject($srcBucket, $srcUri, $bucket, $uri)" to copy the uploaded file to the new name
  5. then delete the original (using deleteObject($bucket, $uri))
  6. then redirect the user to where you want to send them

That will do exactly what you want.


In response to your comments "Is this the only way - what about the costs as Amazon charge per request?"

Delete requests are free. No data transfer costs when moving on the same bucket (or even in the same region). So this solution (which is the only way without you transferring to an intermediate server, renaming and uploading) it doubles the cost of upload a from 1c per 1000 uploads to 2c per 1000 uploads. It's taken me 10 minutes @ $200/hour to find that out and respond = $33 = 1,666,666 uploads! Costs pale a bit when you do the maths :)

Compare with the other solution: do a post to an webserver, rename the file and then upload from the webserver: you move all the bandwidth from the clinet tdirectly to yourself - twice. And this also introduces risk and increased possible failure points.


In response to "Doesn't work. I you upload a file then the old one gets deleted"

I would assusme this is not a problem as you upload a file and then rename it within a second or two. But if you want ot gurantee each file gets uploaded, then you need to do a lot more than create a random filename anyway:

  1. have your "final" bucket
  2. for each upload, create a temporary bucket (that's 1c per 1000 buckets, if you're worried on costs)
  3. upload to temporary bucket
  4. create random name, check if does not exist in final bucket (that 1c per 1000 checks)
  5. copy file to final bucket (with new name)
  6. delete uploaded file as well as the bucket.
  7. periodically clean up buckets where the file uploads were not complete.
Community
  • 1
  • 1
Robbie
  • 17,605
  • 4
  • 35
  • 72
  • Where do I set that piece of code? Just above $params->key = ?. According to your the variable $filanem, it is a scope because if I do $params->key = rand_string(5).'${filename}'; it returns the Dogfilename.png. Correct me if I misunderstood 'Scope'. – jQuerybeast May 19 '12 at 23:41
  • Okay now I get your point. I'll try and do that. I need to find exactly where the putObject is – jQuerybeast May 19 '12 at 23:57
  • Just another point, you can upload normally, and then rename when uploaded? – Robbie May 20 '12 at 00:46
  • As the question states, it is uploaded onto Amazon S3 and thus I am not able to do that. I can upload, copy, replace, delete previous (that will cost me 4 connections instead of 1). – jQuerybeast May 20 '12 at 23:18
  • Yes you can. You can "copy" the file once uploaded on S3 and them delete the old one. All in the Amazon docs. – Robbie May 20 '12 at 23:25
  • Our another solution, create a temporary file add party of the putObject function, (with the new name), upload that file and then delete the temporary file, and you can probably do this by extending the Amazon class, and there's no need to modify the original Amazon sdk., so actually, that would be your best solution. About 10 lines of code, or so. – Robbie May 20 '12 at 23:31
  • I know it is possible, but that will require another 4 connections as stated. 4 connections, if the service is used by 10,000 people; each upload is 40,000 connections for 1 image each. That is not the way to code. – jQuerybeast May 21 '12 at 01:46
  • Although your last edit/answer sounds more like it, I am not sure where to place it. – jQuerybeast May 21 '12 at 02:05
  • Where do you call that Amazon/S3 class? The class isn't going to work on its own - and that's where you edit the call. Where is the file specified? – Robbie May 21 '12 at 02:13
  • From index: http://pastebin.com/qXCVr3eD I call "if (!class_exists('S3')) require_once 'S3.php';" which the S3.php is http://pastebin.com/QAwJphmW – jQuerybeast May 21 '12 at 03:27
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/11491/discussion-between-robbie-and-jquerybeast) – Robbie May 21 '12 at 04:22
  • OK - understand the dilema now I've seen the full code. Final answer updated above. I've loaded a chat window if you're online soon - but short of writing it the full solution, I don't think there's more I (or anyone) can do :) – Robbie May 21 '12 at 04:25
  • Hi there sorry was time for a nap. Is it really no solution without copying and deleting? Every time you send a request to Amazon S3 you get charged. For a service like imgur, they would spend thousands on that point only. – jQuerybeast May 21 '12 at 11:37
  • It is not working. Here is deal, when you upload a file, if a previous exist with the same name it gets replaced. – jQuerybeast May 21 '12 at 20:37
  • But then you rename it? And then you upload the file again and rename it. And then you upload hte same file and rename it. ?!?!? – Robbie May 21 '12 at 21:15
  • Is there any way I get the filename with Javascript, split the extension and pass it onto the php file. – jQuerybeast May 22 '12 at 12:53
  • You don't need to. But you can try it if you want to. – Robbie May 22 '12 at 13:10
  • I am not a PHP developer unfortunately that is why I am struggling now. So to build your answer will take me much more time and effort than trying with Javascript. Although now Javascript doesn't work because you are not allowed to assign another input. – jQuerybeast May 22 '12 at 22:22
3
$fileSplit = explode('.',$filename);
$extension = '.'.$fileSplit[count($fileSplit) - 1];

This explode() divides up the file name into an array with periods as delimiters, and then grabs the last piece of the array (incase a file name is foo.bar.jpg), and puts a period in front of it. This should get you the desired extension to append it to the rand_string(5).

$params->key = rand_string(5).$extension;
Jake M
  • 544
  • 4
  • 13
  • I changed the $filename to '${filename}' and I get this: 3Sf5f.myimage.png – jQuerybeast May 13 '12 at 19:45
  • This means that somewhere down the line, your `$filename` is losing its structure. Look further up in your code, are you changing `$filename` anywhere? – Jake M May 13 '12 at 19:45
  • Notice I'm appending $extension to the params key, not $filename. If you want to override filename after you've got the extension, just do $filename = $extension. – Jake M May 13 '12 at 19:46
  • No filename exists in the entire file for some reason. The entire app is based on 1 file. – jQuerybeast May 13 '12 at 19:48
  • Which lines should I be looking at, this class is HUGE. – Jake M May 13 '12 at 19:51
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/11204/discussion-between-jake-m-and-jquerybeast) – Jake M May 13 '12 at 19:51
2

if you're uploading images try this

$dim = getimagesize($file);
$type = $dim[2];
if( $type == IMAGETYPE_JPEG ) $ext=".jpg"; 
if( $type == IMAGETYPE_GIF ) $ext=".gif"; 
if( $type == IMAGETYPE_PNG ) $ext=".png";

$params->key = rand_string(5).$ext;
Xpleria
  • 5,472
  • 5
  • 52
  • 66
2

I think something as simple as below should work to extract file extension from the file-name:

function getFileExtension($fileName)
{
    $ext = '';
    if(strpos($fileName, ".") !== false)
    {
        $ext = end(explode(".", $fileName));
    }
    return $ext;
}
deej
  • 2,536
  • 4
  • 29
  • 51
1

What happends if you:

$filename = "${filename}";
echo $filename;
die();

Do you get something like 'Dog.png'? If you don't there is something wrong in the way you are getting the filename. If you do get something like 'Dog.png', here is what I use to get the file extension.

$pieces = explode('.',trim($filename));
$extension = end($pieces);

Then you should be able to do this:

$params->key = rand_string(5).'.'.$extension;
Pitchinnate
  • 7,517
  • 1
  • 20
  • 37
  • I played around with some variables and $filename = "${filename}"; is the equivalent of saying $filename = $filename; It doesn't do anything at all, you are setting a variable equal to itself. The variable $filename never even gets set in that function (getHttpUploadPostParams()). I'm guess from the comments on the function that the filename is in an element of $headers. Try doing a print_r($headers); die; before $filename = "${filename}"; and see what you get; – Pitchinnate May 22 '12 at 19:45
  • Wrong. $filename = "${filename}"; is not the same as $filename = $filename. Because I am using $filename further down and the first way i get the actual filename, the second way I dont. – jQuerybeast May 22 '12 at 22:16
  • It is the same thing create a php file and just put this in it: $filename = 'test'; $filename2 = "${filename}"; echo "filename2: $filename2"; You will get "filename2: test" as the output. I understand you are not getting the same thing but there is some other problem in your code. Did you do a print_r($headers); if so what did you get? – Pitchinnate May 24 '12 at 16:03
1

You need to first find out what the original extension is and not rename the entire file. So keep the extension and rename de file name.

Assuming you have image name in $image_name:

$image_name = "image.png";
$random_string = "random";

list($filename,$fileext) = explode(".",$image_name);
$new_name = $random_string.'.'.$fileext;
rename($image_name,$new_name);  
inigomedina
  • 1,791
  • 14
  • 21
1

ok here's another try that I used when I had trouble getting the extension on the server side. what I did was, I used javascript to extract the file extension and the send it via post.

<script type="text/javascript" >
function fileinfo() {
var file = document.form.file.value;
document.getElementById("ext").value = file.split('.').pop();
document.myform.submit();   
}
</script>
<form name="myform" enctype="multipart/form-data" onsubmit="fileinfo();">
<input type="file" name="file">
<input type="hidden" name="ext">
//rest of the form
</form>

in the next php file you can directly use $_POST['ext'] as extension. hope that helped. let me know if you have any trouble implementing this

Xpleria
  • 5,472
  • 5
  • 52
  • 66
  • So on $params->key = $filename; what will I call alongside? – jQuerybeast May 21 '12 at 19:45
  • Invalid according to Policy: Extra input fields: ext – jQuerybeast May 21 '12 at 20:32
  • you could use this directly $params->key = $filename.$_POST['ext']; but I see you can't add an extra field. in that case this solution is not possible. here the extension is evaluated and sent as a part of form data. – Xpleria May 21 '12 at 22:36
1

i am using this in my websites (and works fine for years):

$file = 'yourOriginalfile.png';

//get the file extension
$fileExt = substr(strrchr($file, '.'), 1);

//create a random name and add the original extension
$fileUniqueName = md5(uniqid(mktime())) . '.' . $fileExt;

rename($file,$fileUniqueName); 

your function generates too short filenames (5 characters), this way creates longer filenames, avoiding to collide the file names.

example output: aff5a25e84311485d4eedea7e5f24a4f.png

jacktrade
  • 3,125
  • 2
  • 36
  • 50
  • Short or longer number dont bother that much. Again, your answer wont work if you've read all the answer etc. What if I upload a file with no extension? – jQuerybeast May 22 '12 at 12:51
  • so the problem is in the s3 upload class, i never used this class , but i used this one: the Zend Framework s3 class (you could use it in standalone mode) give a try if you are desperate... http://framework.zend.com/manual/en/zend.service.amazon.s3.html – jacktrade May 23 '12 at 18:20
  • Will that class allow me to reset to anything at any time? – jQuerybeast May 23 '12 at 23:26
  • what do yo mean about "reset"? – jacktrade May 24 '12 at 11:16
  • Im really sorry I was probably thinking of something else. I meant, will that class allow me to change the filename, into any random screen and the file extension? Because it seems my problem comes from Amazon's end. – jQuerybeast May 24 '12 at 21:48
1

It appears what's actually going on is rather than fully producing the filename right now, you're in effect passing a very small 'program' through the interface so it can then produce the filename later (when the variable $filename exists and is in scope). The other side of the interface eventually executes that 'program' you pass in, which produces the modified filename. (Of course passing a 'program' to something else to execute later doesn't tend to make debugging real easy:-)

(It's of course up to you whether you want to "make this work" or "do it a different way". "Different ways" typically involve renaming or copying the file yourself before you even try to invoke the upload interface, and are described in other answers.)

If you decide you want to "make it work", then the entire filename parameter needs to be a program, rather than just part of it. This somewhat uncommon functionality typically involves enclosing the entire string in single quotes. (You also need to do something about existing single quote marks inside the string so they don't terminate the quoted string too soon. One way is to quote each of them with a backslash. Another way that may look cleaner and usually works is to replace them with double quotes.) In other words, I believe the code below will work for you (I haven't got the right environment to test it, so I'm not sure).

$file_extension = 'substr(strrchr(${filename}, "."), 1)';

$params->key = rand_string(5).$file_extension;

(Once you get it working, you might want to revisit your naming scheme. Perhaps the name needs to be a little longer. Or perhaps it should include some identifiable information {like today's date, or the original name of the file}. You may hit on something like $file_base.rand_string(7).$file_extension.

Chuck Kollars
  • 2,135
  • 20
  • 18
0

A simple solution to re-name a file and compute the extension:

$fileName = 'myRandomFile.jpg';

// separate the '.'-separated parts of the file name
$parts = explode( '.', $fileName );

// Solution will not work, if no extension is present
assert( 1 < count( $parts ) );

// keep the extension and drop the last part
$extension = $parts[ count( $parts ) - 1 ];
unset( $parts[ count( $parts ) - 1 ] );

// finally, form the new file name
$newFileName = md5( 'someSeedHere' + implode( '.', $parts )) . '.' . $extension;

echo $extension             // outputs jpg
   . ' - ' 
   . $newFileName           // outputs cfcd208495d565ef66e7dff9f98764da.jpg
   ;

Note, that md5() is always 32 bytes long and non unique regarding the computed value. For for many practical instances, it's unique enough.

Addendum

Additionally, you may use this solution to trace variable changes:

abstract class CSTReportDelegate {

    abstract public function emitVariableChange( $variableName, $oldValue, $newValue );
    abstract public function emitVariableSetNew( $variableName, $newValue );

}

class CSTSimpleReportDelegate extends CSTReportDelegate {

    public function emitVariableChange( $variableName, $oldValue, $newValue ) {
        echo '<br />[global/change] '. $variableName . ' : ' . print_r( $oldValue, true ) . ' &rarr; ' . print_r( $newValue, true );
    }

    public function emitVariableSetNew( $variableName, $newValue ) {
        echo '<br />[global/init] '. $variableName . '   &rarr; ' . print_r( $newValue, TRUE );
    }

}


class CSysTracer {

    static protected 
        $reportDelegate;

    static private 
        $globalState = array();

    static private  
        $traceableGlobals = array();

    static private 
        $globalTraceEnabled = FALSE;

    const 
        DEFAULT_TICK_AMOUNT = 1;

    static public 
    function setReportDelegate( CSTReportDelegate $aDelegate ) {
        self::$reportDelegate = $aDelegate;
    }


    static public 
    function start( $tickAmount = self::DEFAULT_TICK_AMOUNT ) {

        register_tick_function ( array( 'CSysTracer', 'handleTick' ) );

    }


    static public 
    function stop() {

        unregister_tick_function( array( 'CSysTracer', 'handleTick' ) );

    }

    static public 
    function evalAndTrace( $someStatement ) {

        declare( ticks = 1 ); {
            self::start();
            eval( $someStatement );
            self::stop();
        }
    }

    static public 
    function addTraceableGlobal( $varName ) {

        if ( is_array( $varName )) {
            foreach( $varName as $singleName ) {
                self::addTraceableGlobal( $singleName ); 
            }
            return;
        }

        self::$traceableGlobals[ $varName ] = $varName;

    }

    static public 
    function removeTraceableGlobal( $varName ) {
        unset( self::$traceableGlobals[ $varName ] );   
    }

    /**
     * Main function called at each tick. Calls those functions, which
     * really perform the checks.
     * 
     */
    static public 
    function handleTick( ) {

        if ( TRUE === self::$globalTraceEnabled ) { 
            self::traceGlobalVariable();
        }

    }

    static public 
    function enableGlobalsTrace() {
        self::$globalTraceEnabled = TRUE;   
    }


    static public 
    function disableGlobalsTrace() {
        self::$globalTraceEnabled = FALSE;  
    }

    static public 
    function traceGlobalVariable( ) {

        foreach( self::$traceableGlobals as $aVarname ) {

            if ( ! isset( $GLOBALS[ $aVarname ] )) {
                continue;
            }

            if ( ! isset( self::$globalState[ $aVarname ] ) ) {

                self::$reportDelegate->emitVariableSetNew( $aVarname, $GLOBALS[ $aVarname ] );
                self::$globalState[ $aVarname ] = $GLOBALS[ $aVarname ];
                continue;
            }

           if ( self::$globalState[ $aVarname ] !== $GLOBALS[ $aVarname ]) {

             self::$reportDelegate->emitVariableChange( $aVarname, self::$globalState[ $aVarname ], $GLOBALS[ $aVarname ] );

           }

           self::$globalState[ $aVarname ] = $GLOBALS[ $aVarname ];

        }

    }

}

A sample use case:

ini_set("display_errors", TRUE);
error_reporting(E_ALL);

require_once( dirname( __FILE__ ) . '/CStatementTracer.inc.php' );

/* Ticks make it easy to have a function called for every line of PHP
 *  code. We can use this to track the state of a variable throughout
 * the execution of a script.
 */



CSysTracer::addTraceableGlobal( array( 'foo', 'bar' ));

CSysTracer::setReportDelegate( new CSTSimpleReportDelegate() ); 
CSysTracer::enableGlobalsTrace();

CSysTracer::start(); 
declare( ticks = 1 );

   //
   // At this point, tracing is enabled. 
   // Add your code or call your functions/methods here
   //

CSysTracer::stop();
SteAp
  • 11,853
  • 10
  • 53
  • 88
0

Untested, but simple enough to work:

$ext = pathinfo($filename, PATHINFO_EXTENSION);

will return the extension part (without the '.')

J.C. Inacio
  • 4,442
  • 2
  • 22
  • 25
0

How about this?

$temp = rand_string(5).'${filename}';         //should be 3Sf5fDog.png
$ext = pathinfo($temp, PATHINFO_EXTENSION); //should be .png
$temp2 = rand_string(5) . $ext;             //should be 4D47a.png
Tom
  • 2,962
  • 3
  • 39
  • 69
  • After the first line try vardump($temp). Maybe there is something in your string that doesn't normally show up. – Tom May 20 '12 at 14:12