mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 13:32:43 +01:00
Add local setup for S3
This commit is contained in:
committed by
Constantin Graf
parent
fc614b796c
commit
9a8945b0dc
18
.env.example
18
.env.example
@@ -27,7 +27,6 @@ DB_TEST_PASSWORD=root
|
|||||||
|
|
||||||
BROADCAST_DRIVER=log
|
BROADCAST_DRIVER=log
|
||||||
CACHE_DRIVER=file
|
CACHE_DRIVER=file
|
||||||
FILESYSTEM_DISK=local
|
|
||||||
QUEUE_CONNECTION=sync
|
QUEUE_CONNECTION=sync
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
@@ -47,12 +46,6 @@ MAIL_ENCRYPTION=null
|
|||||||
MAIL_FROM_ADDRESS="no-reply@solidtime.test"
|
MAIL_FROM_ADDRESS="no-reply@solidtime.test"
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
S3_ACCESS_KEY_ID=
|
|
||||||
S3_SECRET_ACCESS_KEY=
|
|
||||||
S3_REGION=us-east-1
|
|
||||||
S3_BUCKET=
|
|
||||||
S3_USE_PATH_STYLE_ENDPOINT=false
|
|
||||||
|
|
||||||
PUSHER_APP_ID=
|
PUSHER_APP_ID=
|
||||||
PUSHER_APP_KEY=
|
PUSHER_APP_KEY=
|
||||||
PUSHER_APP_SECRET=
|
PUSHER_APP_SECRET=
|
||||||
@@ -61,6 +54,17 @@ PUSHER_PORT=443
|
|||||||
PUSHER_SCHEME=https
|
PUSHER_SCHEME=https
|
||||||
PUSHER_APP_CLUSTER=mt1
|
PUSHER_APP_CLUSTER=mt1
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
FILESYSTEM_DISK=s3
|
||||||
|
PUBLIC_FILESYSTEM_DISK=s3
|
||||||
|
S3_ACCESS_KEY_ID=sail
|
||||||
|
S3_SECRET_ACCESS_KEY=password
|
||||||
|
S3_REGION=us-east-1
|
||||||
|
S3_BUCKET=local
|
||||||
|
S3_URL=http://storage.solidtime.test/local
|
||||||
|
S3_ENDPOINT=http://storage.solidtime.test
|
||||||
|
S3_USE_PATH_STYLE_ENDPOINT=true
|
||||||
|
|
||||||
VITE_HOST_NAME=vite.solidtime.test
|
VITE_HOST_NAME=vite.solidtime.test
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ return [
|
|||||||
'region' => env('S3_REGION'),
|
'region' => env('S3_REGION'),
|
||||||
'bucket' => env('S3_BUCKET'),
|
'bucket' => env('S3_BUCKET'),
|
||||||
'url' => env('S3_URL'),
|
'url' => env('S3_URL'),
|
||||||
|
'temporary_url' => env('S3_URL'),
|
||||||
'endpoint' => env('S3_ENDPOINT'),
|
'endpoint' => env('S3_ENDPOINT'),
|
||||||
'use_path_style_endpoint' => env('S3_USE_PATH_STYLE_ENDPOINT', false),
|
'use_path_style_endpoint' => env('S3_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
'throw' => true,
|
'throw' => true,
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ services:
|
|||||||
- "traefik.http.routers.solidtime-dev-vite.service=solidtime-dev-vite"
|
- "traefik.http.routers.solidtime-dev-vite.service=solidtime-dev-vite"
|
||||||
- "traefik.http.routers.solidtime-dev-vite.entrypoints=web"
|
- "traefik.http.routers.solidtime-dev-vite.entrypoints=web"
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- 'host.docker.internal:host-gateway'
|
- "host.docker.internal:host-gateway"
|
||||||
|
- "storage.${NGINX_HOST_NAME}:${REVERSE_PROXY_IP:-10.100.100.10}"
|
||||||
environment:
|
environment:
|
||||||
XDG_CONFIG_HOME: /var/www/html/config
|
XDG_CONFIG_HOME: /var/www/html/config
|
||||||
XDG_DATA_HOME: /var/www/html/data
|
XDG_DATA_HOME: /var/www/html/data
|
||||||
@@ -128,6 +129,63 @@ services:
|
|||||||
- reverse-proxy
|
- reverse-proxy
|
||||||
volumes:
|
volumes:
|
||||||
- '.:/src'
|
- '.:/src'
|
||||||
|
minio:
|
||||||
|
image: 'minio/minio:latest'
|
||||||
|
environment:
|
||||||
|
MINIO_BROWSER_REDIRECT_URL: 'https://storage-management.${NGINX_HOST_NAME}'
|
||||||
|
MINIO_ROOT_USER: 'sail'
|
||||||
|
MINIO_ROOT_PASSWORD: 'password'
|
||||||
|
volumes:
|
||||||
|
- 'sail-minio:/data/minio'
|
||||||
|
networks:
|
||||||
|
- reverse-proxy
|
||||||
|
- sail
|
||||||
|
command: minio server /data/minio --console-address ":8900"
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "mc", "ready", "local" ]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=${NETWORK_NAME}"
|
||||||
|
# Storage Frontend
|
||||||
|
- "traefik.http.services.solidtime-dev-storage-frontend.loadbalancer.server.port=9000"
|
||||||
|
# http
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-frontend.rule=Host(`storage.${NGINX_HOST_NAME}`)"
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-frontend.service=solidtime-dev-storage-frontend"
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-frontend.entrypoints=web"
|
||||||
|
# https
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-frontend-https.rule=Host(`storage.${NGINX_HOST_NAME}`)"
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-frontend-https.service=solidtime-dev-storage-frontend"
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-frontend-https.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-frontend-https.tls=true"
|
||||||
|
# Storage Management
|
||||||
|
- "traefik.http.services.solidtime-dev-storage-management.loadbalancer.server.port=8900"
|
||||||
|
# http
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-management.rule=Host(`storage-management.${NGINX_HOST_NAME}`)"
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-management.service=solidtime-dev-storage-management"
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-management.entrypoints=web"
|
||||||
|
# https
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-management-https.rule=Host(`storage-management.${NGINX_HOST_NAME}`)"
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-management-https.service=solidtime-dev-storage-management"
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-management-https.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.solidtime-dev-storage-management-https.tls=true"
|
||||||
|
|
||||||
|
minio-create-bucket:
|
||||||
|
image: minio/mc:latest
|
||||||
|
depends_on:
|
||||||
|
- minio
|
||||||
|
environment:
|
||||||
|
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
|
||||||
|
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
|
||||||
|
S3_BUCKET: ${S3_BUCKET}
|
||||||
|
S3_ENDPOINT: ${S3_ENDPOINT}
|
||||||
|
volumes:
|
||||||
|
- './docker/local/minio:/etc/minio'
|
||||||
|
networks:
|
||||||
|
- sail
|
||||||
|
entrypoint: /etc/minio/create_bucket.sh
|
||||||
networks:
|
networks:
|
||||||
reverse-proxy:
|
reverse-proxy:
|
||||||
name: "${NETWORK_NAME}"
|
name: "${NETWORK_NAME}"
|
||||||
@@ -139,3 +197,5 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
sail-pgsql-test:
|
sail-pgsql-test:
|
||||||
driver: local
|
driver: local
|
||||||
|
sail-minio:
|
||||||
|
driver: local
|
||||||
|
|||||||
10
docker/local/minio/create_bucket.sh
Executable file
10
docker/local/minio/create_bucket.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Source: https://helgesver.re/articles/laravel-sail-create-minio-bucket-automatically
|
||||||
|
|
||||||
|
/usr/bin/mc config host add local ${S3_ENDPOINT} ${S3_ACCESS_KEY_ID} ${S3_SECRET_ACCESS_KEY};
|
||||||
|
/usr/bin/mc rm -r --force local/${S3_BUCKET};
|
||||||
|
/usr/bin/mc mb --ignore-existing local/${S3_BUCKET};
|
||||||
|
/usr/bin/mc anonymous set public local/${S3_BUCKET};
|
||||||
|
|
||||||
|
exit 0;
|
||||||
@@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Collection;
|
|||||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Mockery\MockInterface;
|
use Mockery\MockInterface;
|
||||||
use TiMacDonald\Log\LogFake;
|
use TiMacDonald\Log\LogFake;
|
||||||
|
|
||||||
@@ -25,6 +26,16 @@ abstract class TestCase extends BaseTestCase
|
|||||||
LogFake::bind();
|
LogFake::bind();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function mockPrivateStorage(): void
|
||||||
|
{
|
||||||
|
Storage::fake(config('filesystems.default'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function mockPublicStorage(): void
|
||||||
|
{
|
||||||
|
Storage::fake(config('filesystems.public'));
|
||||||
|
}
|
||||||
|
|
||||||
protected function tearDown(): void
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
// Note: It is necessary to clear the permission cache after each test, since the "scoped singletons" are not reset between tests.
|
// Note: It is necessary to clear the permission cache after each test, since the "scoped singletons" are not reset between tests.
|
||||||
|
|||||||
@@ -81,13 +81,13 @@ class ExportEndpointTest extends ApiEndpointTestAbstract
|
|||||||
Passport::actingAs($user->user);
|
Passport::actingAs($user->user);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$response = $this->postJson(route('api.v1.export.export', ['organization' => $user->organization->getKey()]));
|
$response = $this->postJson(route('api.v1.export.export', [
|
||||||
|
'organization' => $user->organization->getKey(),
|
||||||
|
]));
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
$response->assertExactJson([
|
$response->assertJsonPath('success', true);
|
||||||
'success' => true,
|
$this->assertStringContainsString($filepath, $response->json('download_url'));
|
||||||
'download_url' => Storage::disk('local')->temporaryUrl($filepath, $now->addMinutes(10)),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,10 +261,11 @@ class DeletionServiceTest extends TestCaseWithDatabase
|
|||||||
public function test_delete_user_deletes_all_resources_of_the_user_but_does_not_delete_other_resources(): void
|
public function test_delete_user_deletes_all_resources_of_the_user_but_does_not_delete_other_resources(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
$this->mockPublicStorage();
|
||||||
$user = User::factory()->withProfilePicture()->withPersonalOrganization()->create();
|
$user = User::factory()->withProfilePicture()->withPersonalOrganization()->create();
|
||||||
$otherUser = User::factory()->withProfilePicture()->withPersonalOrganization()->create();
|
$otherUser = User::factory()->withProfilePicture()->withPersonalOrganization()->create();
|
||||||
Storage::disk('public')->assertExists($user->profile_photo_path);
|
Storage::disk(config('filesystems.public'))->assertExists($user->profile_photo_path);
|
||||||
Storage::disk('public')->assertExists($otherUser->profile_photo_path);
|
Storage::disk(config('filesystems.public'))->assertExists($otherUser->profile_photo_path);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$this->deletionService->deleteUser($user);
|
$this->deletionService->deleteUser($user);
|
||||||
@@ -288,8 +289,8 @@ class DeletionServiceTest extends TestCaseWithDatabase
|
|||||||
$this->assertDatabaseMissing(Member::class, [
|
$this->assertDatabaseMissing(Member::class, [
|
||||||
'user_id' => $user->getKey(),
|
'user_id' => $user->getKey(),
|
||||||
]);
|
]);
|
||||||
Storage::disk('public')->assertMissing($user->profile_photo_path);
|
Storage::disk(config('filesystems.public'))->assertMissing($user->profile_photo_path);
|
||||||
Storage::disk('public')->assertExists($otherUser->profile_photo_path);
|
Storage::disk(config('filesystems.public'))->assertExists($otherUser->profile_photo_path);
|
||||||
Log::assertLoggedTimes(fn (LogEntry $log) => $log->level === 'debug'
|
Log::assertLoggedTimes(fn (LogEntry $log) => $log->level === 'debug'
|
||||||
&& $log->message === 'Start deleting user'
|
&& $log->message === 'Start deleting user'
|
||||||
&& $log->context['id'] === $user->getKey(),
|
&& $log->context['id'] === $user->getKey(),
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class ExportServiceTest extends TestCaseWithDatabase
|
|||||||
public function test_export_creates_zip_with_all_the_data_of_the_organization(): void
|
public function test_export_creates_zip_with_all_the_data_of_the_organization(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
$this->mockPrivateStorage();
|
||||||
$organization1 = $this->getFullOrganization();
|
$organization1 = $this->getFullOrganization();
|
||||||
$organization2 = $this->getFullOrganization();
|
$organization2 = $this->getFullOrganization();
|
||||||
|
|
||||||
@@ -61,6 +62,6 @@ class ExportServiceTest extends TestCaseWithDatabase
|
|||||||
$zip = $exportService->export($organization1);
|
$zip = $exportService->export($organization1);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Storage::disk('local')->assertExists($zip);
|
Storage::disk(config('filesystems.default'))->assertExists($zip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Models\Organization;
|
|||||||
use App\Service\Import\Importers\DefaultImporter;
|
use App\Service\Import\Importers\DefaultImporter;
|
||||||
use App\Service\Import\Importers\ImportException;
|
use App\Service\Import\Importers\ImportException;
|
||||||
use App\Service\Import\Importers\TogglTimeEntriesImporter;
|
use App\Service\Import\Importers\TogglTimeEntriesImporter;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\UsesClass;
|
use PHPUnit\Framework\Attributes\UsesClass;
|
||||||
@@ -28,10 +29,14 @@ class TogglTimeEntriesImporterTest extends ImporterTestAbstract
|
|||||||
$data = Storage::disk('testfiles')->get('toggl_time_entries_import_test_1.csv');
|
$data = Storage::disk('testfiles')->get('toggl_time_entries_import_test_1.csv');
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
DB::enableQueryLog();
|
||||||
|
DB::flushQueryLog();
|
||||||
$importer->importData($data, $timezone);
|
$importer->importData($data, $timezone);
|
||||||
$report = $importer->getReport();
|
$report = $importer->getReport();
|
||||||
|
$queryLog = DB::getQueryLog();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
$this->assertCount(31, $queryLog);
|
||||||
$testScenario = $this->checkTestScenarioAfterImportExcludingTimeEntries();
|
$testScenario = $this->checkTestScenarioAfterImportExcludingTimeEntries();
|
||||||
$this->checkTimeEntries($testScenario);
|
$this->checkTimeEntries($testScenario);
|
||||||
$this->assertSame(2, $report->timeEntriesCreated);
|
$this->assertSame(2, $report->timeEntriesCreated);
|
||||||
@@ -55,10 +60,14 @@ class TogglTimeEntriesImporterTest extends ImporterTestAbstract
|
|||||||
$importer->init($organization);
|
$importer->init($organization);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
DB::enableQueryLog();
|
||||||
|
DB::flushQueryLog();
|
||||||
$importer->importData($data, $timezone);
|
$importer->importData($data, $timezone);
|
||||||
$report = $importer->getReport();
|
$report = $importer->getReport();
|
||||||
|
$queryLog = DB::getQueryLog();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
$this->assertCount(15, $queryLog);
|
||||||
$testScenario = $this->checkTestScenarioAfterImportExcludingTimeEntries();
|
$testScenario = $this->checkTestScenarioAfterImportExcludingTimeEntries();
|
||||||
$this->checkTimeEntries($testScenario, true);
|
$this->checkTimeEntries($testScenario, true);
|
||||||
$this->assertSame(2, $report->timeEntriesCreated);
|
$this->assertSame(2, $report->timeEntriesCreated);
|
||||||
|
|||||||
Reference in New Issue
Block a user