Solution #1 is viable but only if the source bucket is versioned and the Lambda function writes to a database that supports strongly consistent reads, because that's the only way you can tell authoritatively whether you're doing it "only once for the first time."
When you ask S3 about the existence of an object for the very first time ever using a GetObjectMetadata/HeadObject request, you are guaranteed to get an accurate answer -- the object exists, or it doesn't. Subsequent requests within a "short" (but undocumented) time may not reflect the latest truth from the bucket's master index.
Amazon S3 provides read-after-write consistency for PUTS of new objects in your S3 bucket in all Regions with one caveat. The caveat is that if you make a HEAD or GET request to a key name before the object is created, then create the object shortly after that, a subsequent GET might not return the object due to eventual consistency.
https://docs.aws.amazon.com/AmazonS3/latest/dev/Introduction.html
"a subsequent GET might not return the object" means by inference that the Lambda invocation for upload #2 has no guaranteed way to authoritatively determine that it is indeed not handling the first upload of the object, because the target object may appear to be absent when it is in fact already present.
If it is of significant importance that only one upload -- the first upload -- actually be used, then an external database is required.
The Lambda cold start issue is pretty insignificant in languages with a lightweight environment like Javascript, which coincidentally has an SDK that is much better written than most other languages. Python is fast, too, but the SDK is inferior. Java should be avoided in Lambda functions unless you are repurposing existing Java code.
This would be the solution I would adopt, but I would note that the 15 minute expiration should be unnecessary. The expiration time of a pre-signed URL is as precise as the clock on the system running the code that generated the URL, so if your server clock is accurate and you generate a URL that expires in 10 seconds, then that URL expires in exactly 10 seconds from the time it was created.
Also potentially noteworthy, the expiration time on a pre-signed URL is checked when the request arrives at S3. You shouldn't need to finish the upload in 10 seconds, you only need to start the upload within 10 seconds. This is at least true for download pre-signed URLs... but should be true for uploads as well.
Modifying your code to look at a specific version is a non-starter, because querying S3 to learn that version ID each time is slow and costly.