Skip to content

Conversation

@LiquidPL
Copy link
Contributor

@LiquidPL LiquidPL commented Jan 7, 2026

Depends on #12618. Recreated the original PR (#12623) to pull in changes from base PR code review and some cleanup.

Steps to deploy:

  • Set cutoff screenshot ID for "legacy" screenshots without hash in the SCREENSHOTS_LEGACY_ID_CUTOFF env var.
  • [OPTIONAL] Switch production to use these endpoints instead of legacy web if everything works correctly.

Comment on lines 39 to 46
abort_if(Screenshot::urlHash(intval($id)) !== $hash, 404);

return $this->showBase($id);
}

public function showLegacy(int $id)
{
abort_if(!Screenshot::isLegacyId(intval($id)), 404);
Copy link
Collaborator

@nanaya nanaya Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the routes can be combined with a lookup function in the model which does something like

if hash == null
  if id > legacy_id
    return null
else
  expectedHash = ...
  if !hash_equals(expectedHash, hash)
    return null

return findOrFail(...)

Comment on lines 54 to 57
$screenshot->update([
'hits' => $screenshot->hits + 1,
'last_access' => Carbon::now(),
]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use incrementInstance('hits', 1, ['last_access' => ...])


return response()->stream(function () use ($file) {
echo $file;
}, 200, ['Content-Type' => 'image/jpeg']);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need cache-control header too (it's missing in the current one)


$file = $screenshot->fetch();

abort_if(!$file, 404);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't really matter here but generally should avoid casting php string to boolean (explicitly check with === null instead)


$user = User::factory()->create();
$screenshot = Screenshot::factory()->create(['user_id' => $user->getKey()]);
\Storage::disk('local-screenshot')->putFileAs('/', UploadedFile::fake()->image('ss.jpg'), "{$screenshot->getKey()}.jpg");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use the existing store function?

$this->assertGreaterThan(0, preg_match('/https?:\/\/.+\/(\d+)\/(.{4})/', $url, $matches));

\Storage::disk('local-screenshot')->assertExists("{$matches[1]}.jpg");
$this->assertEquals(Screenshot::urlHash(intval($matches[1])), $matches[2]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use assertSame


$matches = [];

$this->assertGreaterThan(0, preg_match('/https?:\/\/.+\/(\d+)\/(.{4})/', $url, $matches));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a different regexp delimiter so / doesn't need to be escaped. No need to match https? (or explicitly match the start), but the end should probably be matched explicitly with $


$this->assertGreaterThan(0, preg_match('/https?:\/\/.+\/(\d+)\/(.{4})/', $url, $matches));

\Storage::disk('local-screenshot')->assertExists("{$matches[1]}.jpg");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or get the object and check its fetch result instead of relying on specific file structure

.env.example Outdated

# S3_SCREENSHOT_BUCKET=
# SCREENSHOTS_SHARED_SECRET=
# SCREENSHOTS_LEGACY_ID_CUTOFF=
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot in the other pr but nowadays the example should have the default value


public function testShow()
{
$user = User::factory()->create();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apart for the store test, this doesn't do anything

if (!self::isLegacyId($id)) {
return null;
}
} else if (self::hashForId($id) !== $hash) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use hash_equals when comparing secrets

public function hash(): ?string
{
return substr(md5($this->getKey().$GLOBALS['cfg']['osu']['screenshots']['shared_secret']), 0, 4);
if (self::isLegacyId($this->getKey())) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed

$this->actAsScopedUser($user);

$this->postJson(route('api.screenshots.store'), [
\Storage::fake('local-screenshot');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might as well put this in setUp

@nanaya nanaya enabled auto-merge January 9, 2026 09:37
@nanaya nanaya merged commit ac51aaa into ppy:master Jan 9, 2026
3 checks passed
@LiquidPL LiquidPL deleted the show-screenshot2 branch January 9, 2026 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants