Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
80926da
Fix upload-directory command
iamlili Jun 1, 2021
6660495
Merge pull request #526 from humanmade/backport-521-to-v3-branch
roborourke Jun 15, 2021
d47a596
Merge pull request #529 from humanmade/master
joehoyle Jun 16, 2021
8a1262d
Merge pull request #542 from humanmade/master
roborourke Jul 30, 2021
fc88fbd
Added get_s3_path function instead of hard coding s3 path.
johnrom Feb 15, 2022
8ad534d
Merge pull request #567 from humanmade/master
joehoyle Mar 2, 2022
425e5fe
Use ListObjectsV2 where applicable
kovshenin Nov 21, 2022
776f6d0
Merge pull request #613 from humanmade/backport-609-to-v3-branch
kovshenin Nov 28, 2022
56e5954
Add composer/installers to allow-plugins
kovshenin Nov 28, 2022
1f3a7a2
Merge pull request #615 from humanmade/backport-614-to-v3-branch
kovshenin Nov 29, 2022
b65f2c7
Merge pull request #632 from humanmade/backport-564-to-v3-branch
joehoyle May 4, 2023
17e9716
Replace "/" with "\" on windows machines
rasmuswinter May 22, 2023
4693129
Adding return type
rasmuswinter May 22, 2023
3360ede
Merge pull request #636 from outlandishideas/local-windows-filesystem…
joehoyle Sep 26, 2024
5969d61
Add Stream support for audit logs of all ACL sets done by S3 Uploads …
Rayhatron May 30, 2025
4089f71
Merge pull request #696 from humanmade/backport-audit-logging
joehoyle Jun 3, 2025
55d3b5d
Scandir issue #634 and #630
ramonfincken Mar 31, 2025
52eac94
Update class-plugin.php
ramonfincken Apr 15, 2025
b979a2c
Move tests to github
joehoyle Jun 3, 2025
cf6184b
Supress warning
joehoyle Jun 3, 2025
81cdb36
Remove travis file
joehoyle Jun 3, 2025
27ee574
Fix psalm
joehoyle Jun 3, 2025
0c874db
Add phpunit polyfills
joehoyle Jun 3, 2025
e220788
Different way to get factory?
joehoyle Jun 3, 2025
2873072
Bump required PHP version
joehoyle Jun 3, 2025
7ad6fdc
Update build images
joehoyle Jun 3, 2025
d798007
FIx url
joehoyle Jun 3, 2025
f5e9a77
Merge pull request #701 from humanmade/backport-687
joehoyle Jun 6, 2025
99f4de1
Merge pull request #702 from humanmade/backport-698-to-v3-branch
joehoyle Jun 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI

on:
push:
branches: [ master, v3-branch ]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install dependencies
run: docker run --rm -v $PWD:/code --entrypoint='' humanmade/plugin-tester composer install

- name: Run tests
run: ./tests/run-tests.sh --coverage-clover=coverage.xml

- name: Upload coverage to Codecov
run: bash <(curl -s https://codecov.io/bash)
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
19 changes: 0 additions & 19 deletions .travis.yml

This file was deleted.

16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
Lightweight "drop-in" for storing WordPress uploads on Amazon S3 instead of the local filesystem.
</td>
<td align="right" width="20%">
<a href="https://shepherd.dev/github/humanmade/S3-Uploads/">
<a href="https://shepherd.dev/github/humanmade/S3-Uploads">
<img src="https://shepherd.dev/github/humanmade/S3-Uploads/coverage.svg" alt="Psalm coverage">
</a>
<a href="https://travis-ci.com/humanmade/S3-Uploads">
<img src="https://travis-ci.com/humanmade/S3-Uploads.svg?branch=master" alt="Build status">
<a href="https://github.com/humanmade/S3-Uploads/actions/workflows/ci.yml">
<img src="https://github.com/humanmade/S3-Uploads/actions/workflows/ci.yml/badge.svg" alt="CI">
</a>
<a href="http://codecov.io/github/humanmade/S3-Uploads?branch=master">
<img src="http://codecov.io/github/humanmade/S3-Uploads/coverage.svg?branch=master" alt="Coverage via codecov.io" />
<a href="https://codecov.io/github/humanmade/S3-Uploads" >
<img src="https://codecov.io/github/humanmade/S3-Uploads/graph/badge.svg?token=JmeqBWddkV"/>
</a>
</td>
</tr>
Expand All @@ -32,7 +32,7 @@ It's focused on providing a highly robust S3 interface with no "bells and whistl

## Requirements

- PHP >= 7.1
- PHP >= 7.4
- WordPress >= 5.3

## Getting Set Up
Expand Down Expand Up @@ -133,7 +133,7 @@ Note: as either `<from>` or `<to>` can be S3 or local locations, you must specif

## Private Uploads

WordPress (and therefor S3 Uploads) default behaviour is that all uploaded media files are publicly accessible. In certain cases which may not be desireable. S3 Uploads supports setting S3 Objects to a `private` ACL and providing temporarily signed URLs for all files that are marked as private.
WordPress (and therefore S3 Uploads) default behaviour is that all uploaded media files are publicly accessible. In certain cases which may not be desireable. S3 Uploads supports setting S3 Objects to a `private` ACL and providing temporarily signed URLs for all files that are marked as private.

S3 Uploads does not make assumptions or provide UI for marking attachments as private, instead you should integrate the `s3_uploads_is_attachment_private` WordPress filter to control the behaviour. For example, to mark _all_ attachments as private:

Expand All @@ -155,6 +155,8 @@ add_filter( 's3_uploads_private_attachment_url_expiry', function ( $expiry ) {
} );
```

If you're using [Stream](https://wordpress.org/plugins/stream/) for audit logs, [S3 Uploads Audit](https://github.com/humanmade/s3-uploads-audit) is an add-on plugin which supports logging some S3 Uploads actions e.g any setting of ACL for files of an attachment. So you can install it for such audit functionality.

## Cache Control

You can define the default HTTP `Cache-Control` header for uploaded media using the
Expand Down
8 changes: 7 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@
"require-dev": {
"phpunit/phpunit": "7.5",
"pcov/clobber": "^2.0",
"humanmade/psalm-plugin-wordpress": "^1.0"
"humanmade/psalm-plugin-wordpress": "^1.0",
"yoast/phpunit-polyfills": "^4.0"
},
"scripts": {
"test": "./tests/run-tests.sh",
"check-types": "./vendor/bin/psalm"
},
"config": {
"allow-plugins": {
"composer/installers": true
}
}
}
2 changes: 1 addition & 1 deletion inc/class-local-stream-wrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
$uri = $this->uri;
}
$path = $this->getDirectoryPath() . '/' . $this->getTarget( $uri );
$realpath = $path;
$realpath = str_replace( '/', DIRECTORY_SEPARATOR, $path ); // ensure check against realpath passes on Windows machines

Check failure on line 146 in inc/class-local-stream-wrapper.php

View check run for this annotation

HM Linter / hmlinter

inc/class-local-stream-wrapper.php#L146

Tabs must be used to indent lines; spaces are not allowed
Raw output
{
  "line": 146,
  "column": 1,
  "severity": "error",
  "message": "Tabs must be used to indent lines; spaces are not allowed",
  "source": "Generic.WhiteSpace.DisallowSpaceIndent.SpacesUsed"
}

$directory = realpath( $this->getDirectoryPath() );

Expand Down
35 changes: 28 additions & 7 deletions inc/class-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ public function register_stream_wrapper() {
stream_context_set_option( stream_context_get_default(), 's3', 'seekable', true );
}

/**
* Get the s3:// path for the bucket.
*/
public function get_s3_path() : string {
return 's3://' . $this->bucket;
}

/**
* Overwrite the default wp_upload_dir.
*
Expand All @@ -163,19 +170,20 @@ public function register_stream_wrapper() {
public function filter_upload_dir( array $dirs ) : array {

$this->original_upload_dir = $dirs;
$s3_path = $this->get_s3_path();

$dirs['path'] = str_replace( WP_CONTENT_DIR, 's3://' . $this->bucket, $dirs['path'] );
$dirs['basedir'] = str_replace( WP_CONTENT_DIR, 's3://' . $this->bucket, $dirs['basedir'] );
$dirs['path'] = str_replace( WP_CONTENT_DIR, $s3_path, $dirs['path'] );
$dirs['basedir'] = str_replace( WP_CONTENT_DIR, $s3_path, $dirs['basedir'] );

if ( ! defined( 'S3_UPLOADS_DISABLE_REPLACE_UPLOAD_URL' ) || ! S3_UPLOADS_DISABLE_REPLACE_UPLOAD_URL ) {

if ( defined( 'S3_UPLOADS_USE_LOCAL' ) && S3_UPLOADS_USE_LOCAL ) {
$dirs['url'] = str_replace( 's3://' . $this->bucket, $dirs['baseurl'] . '/s3/' . $this->bucket, $dirs['path'] );
$dirs['baseurl'] = str_replace( 's3://' . $this->bucket, $dirs['baseurl'] . '/s3/' . $this->bucket, $dirs['basedir'] );
$dirs['url'] = str_replace( $s3_path, $dirs['baseurl'] . '/s3/' . $this->bucket, $dirs['path'] );
$dirs['baseurl'] = str_replace( $s3_path, $dirs['baseurl'] . '/s3/' . $this->bucket, $dirs['basedir'] );

} else {
$dirs['url'] = str_replace( 's3://' . $this->bucket, $this->get_s3_url(), $dirs['path'] );
$dirs['baseurl'] = str_replace( 's3://' . $this->bucket, $this->get_s3_url(), $dirs['basedir'] );
$dirs['url'] = str_replace( $s3_path, $this->get_s3_url(), $dirs['path'] );
$dirs['baseurl'] = str_replace( $s3_path, $this->get_s3_url(), $dirs['basedir'] );
}
}

Expand Down Expand Up @@ -525,6 +533,15 @@ public function set_attachment_files_acl( int $attachment_id, string $acl ) : ?W
return new WP_Error( $e->getCode(), $e->getMessage() );
}

/**
* Fires after ACL of files of an attachment is set.
*
* @param int $attachment_id Attachment whose ACL has been changed.
* @param string $acl The new ACL that's been set.
* @psalm-suppress TooManyArguments -- Currently do_action doesn't detect variable number of arguments.
*/
do_action( 's3_uploads_set_attachment_files_acl', $attachment_id, $acl );

return null;
}

Expand Down Expand Up @@ -665,6 +682,10 @@ public function get_files_for_unique_filename_file_list( ?array $files, string $
$name = pathinfo( $filename, PATHINFO_FILENAME );
// The s3:// streamwrapper support listing by partial prefixes with wildcards.
// For example, scandir( s3://bucket/2019/06/my-image* )
return scandir( trailingslashit( $dir ) . $name . '*' );
$scandir = scandir( trailingslashit( $dir ) . $name . '*' );
if ( $scandir === false ) {
$scandir = []; // Set as empty array for return
}
return $scandir;
}
}
8 changes: 4 additions & 4 deletions inc/class-stream-wrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ function () use ( $parts, $path ) {
} catch ( S3Exception $e ) {
// Maybe this isn't an actual key, but a prefix. Do a prefix
// listing of objects to determine.
$result = $this->getClient()->listObjects(
$result = $this->getClient()->listObjectsV2(
[
'Bucket' => $parts['Bucket'],
'Prefix' => rtrim( $parts['Key'], '/' ) . '/',
Expand Down Expand Up @@ -615,7 +615,7 @@ public function dir_opendir( $path, $options ) {
// the preg_match() call.
//
// Essentially, wp_unique_filename( my-file.jpg ) doing a `scandir( s3://bucket/2019/04/ )` will actually result in an s3
// listObject query for `s3://bucket/2019/04/my-file` which means even if there are millions of files in `2019/04/` we only
// listObjectsV2 query for `s3://bucket/2019/04/my-file` which means even if there are millions of files in `2019/04/` we only
// return a much smaller subset.
//
// Anyone reading this far, brace yourselves for a mighty horrible hack.
Expand All @@ -635,7 +635,7 @@ public function dir_opendir( $path, $options ) {
// Filter our "/" keys added by the console as directories, and ensure
// that if a filter function is provided that it passes the filter.
$this->objectIterator = \Aws\flatmap(
$this->getClient()->getPaginator( 'ListObjects', $op ),
$this->getClient()->getPaginator( 'ListObjectsV2', $op ),
function ( Result $result ) use ( $filterFn ) {
/** @var list<S3ObjectResultArray> */
$contentsAndPrefixes = $result->search( '[Contents[], CommonPrefixes[]][]' );
Expand Down Expand Up @@ -1087,7 +1087,7 @@ private function deleteSubfolder( string $path, array $params ) : bool {
// Use a key that adds a trailing slash if needed.
$prefix = rtrim( $params['Key'], '/' ) . '/';
/** @var array{Contents: list<array{ Key: string }>, CommonPrefixes:array} */
$result = $this->getClient()->listObjects(
$result = $this->getClient()->listObjectsV2(
[
'Bucket' => $params['Bucket'],
'Prefix' => $prefix,
Expand Down
2 changes: 1 addition & 1 deletion inc/class-wp-cli-command.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public function ls( array $args ) {

try {
$objects = $s3->getIterator(
'ListObjects', [
'ListObjectsV2', [
'Bucket' => strtok( S3_UPLOADS_BUCKET, '/' ),
'Prefix' => $prefix,
]
Expand Down
2 changes: 1 addition & 1 deletion tests/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mkdir /tmp/s3-uploads-tests/tests

docker run --rm --name s3-uploads-tests-minio -d --rm -p 9000:9000 -e MINIO_ACCESS_KEY=AWSACCESSKEY -e MINIO_SECRET_KEY=AWSSECRETKEY -v /tmp/s3-uploads-tests:/data minio/minio server /data > /dev/null

docker run --rm -e S3_UPLOADS_BUCKET=tests -e S3_UPLOADS_KEY=AWSACCESSKEY -e S3_UPLOADS_SECRET=AWSSECRETKEY -e S3_UPLOADS_REGION=us-east-1 -v $PWD:/code humanmade/plugin-tester $@
docker run --rm -e AWS_SUPPRESS_PHP_DEPRECATION_WARNING=1 -e S3_UPLOADS_BUCKET=tests -e S3_UPLOADS_KEY=AWSACCESSKEY -e S3_UPLOADS_SECRET=AWSSECRETKEY -e S3_UPLOADS_REGION=us-east-1 -v $PWD:/code humanmade/plugin-tester $@
docker kill s3-uploads-tests-minio > /dev/null

echo "Running Psalm..."
Expand Down
8 changes: 4 additions & 4 deletions tests/test-s3-uploads.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public function test_generate_attachment_metadata() {
$upload_dir = wp_upload_dir();
copy( dirname( __FILE__ ) . '/data/sunflower.jpg', $upload_dir['path'] . '/sunflower.jpg' );
$test_file = $upload_dir['path'] . '/sunflower.jpg';
$attachment_id = $this->factory->attachment->create_object( $test_file, 0, array(
$attachment_id = self::factory()->attachment->create_object( $test_file, 0, array(
'post_mime_type' => 'image/jpeg',
'post_excerpt' => 'A sample caption',
) );
Expand All @@ -99,7 +99,7 @@ public function test_generate_attachment_metadata_for_mp4() {
$upload_dir = wp_upload_dir();
copy( dirname( __FILE__ ) . '/data/video.m4v', $upload_dir['path'] . '/video.m4v' );
$test_file = $upload_dir['path'] . '/video.m4v';
$attachment_id = $this->factory->attachment->create_object( $test_file, 0, array(
$attachment_id = self::factory()->attachment->create_object( $test_file, 0, array(
'post_mime_type' => 'video/mp4',
'post_excerpt' => 'A sample caption',
) );
Expand All @@ -116,7 +116,7 @@ public function test_image_sizes_are_deleted_on_attachment_delete() {
$upload_dir = wp_upload_dir();
copy( dirname( __FILE__ ) . '/data/sunflower.jpg', $upload_dir['path'] . '/sunflower.jpg' );
$test_file = $upload_dir['path'] . '/sunflower.jpg';
$attachment_id = $this->factory->attachment->create_object( $test_file, 0, array(
$attachment_id = self::factory()->attachment->create_object( $test_file, 0, array(
'post_mime_type' => 'image/jpeg',
'post_excerpt' => 'A sample caption',
) );
Expand All @@ -138,7 +138,7 @@ public function test_generate_attachment_metadata_for_pdf() {
$upload_dir = wp_upload_dir();
copy( dirname( __FILE__ ) . '/data/gdpr.pdf', $upload_dir['path'] . '/gdpr.pdf' );
$test_file = $upload_dir['path'] . '/gdpr.pdf';
$attachment_id = $this->factory->attachment->create_object( $test_file, 0, array(
$attachment_id = self::factory()->attachment->create_object( $test_file, 0, array(
'post_mime_type' => 'application/pdf',
'post_excerpt' => 'A sample caption',
) );
Expand Down
Loading