Skip to content

Commit

Permalink
Merge pull request #1018 from biigle/merge-sync
Browse files Browse the repository at this point in the history
Merge biigle/sync
  • Loading branch information
mzur authored Dec 20, 2024
2 parents 1ffbf0b + ae206a0 commit 725930d
Show file tree
Hide file tree
Showing 94 changed files with 9,352 additions and 0 deletions.
33 changes: 33 additions & 0 deletions app/Console/Commands/SyncPrune.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Biigle\Console\Commands;

use Biigle\Services\Import\ArchiveManager;
use Illuminate\Console\Command;

class SyncPrune extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'sync:prune';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete uploaded import files that are older than one week and where the import was not finished.';

/**
* Execute the command.
*
* @return void
*/
public function handle(ArchiveManager $manager)
{
$manager->prune();
}
}
123 changes: 123 additions & 0 deletions app/Console/Commands/SyncUuids.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

namespace Biigle\Console\Commands;

use Biigle\User;
use File;
use Hash;
use Illuminate\Console\Command;

class SyncUuids extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'sync:uuids
{file? : If a file is provided, use it for syncing. If not, output the contents of such a file.}
{--dry-run : Do not change the database records}
{--force : Synchronise matching users without asking}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronize user UUIDs across BIIGLE instances';

/**
* Execute the command.
*
* @return void
*/
public function handle()
{
if ($this->argument('file')) {
$this->handleSync();
} else {
$this->generateOutput();
}
}

/**
* Read the input file and perform interactive synchronization of user UUIDs.
*/
protected function handleSync()
{
$matches = 0;
$input = collect(json_decode(File::get($this->argument('file')), true));
$inputUuids = $input->pluck('uuid');

// All users with a UUID not among those of the input are candidates for syncing.
$users = User::select('id', 'email', 'firstname', 'lastname')
->whereNotIn('uuid', $inputUuids)
->get();

// Remove input users with a UUID already in the database as they should not be
// considered for syncing.
$input = $input->whereNotIn('uuid', User::whereIn('uuid', $inputUuids)->pluck('uuid'));

// Find matches and sync.
foreach ($users as $user) {
foreach ($input as $inputUser) {
$emailMatches = Hash::check($user->email, $inputUser['email']);
$nameMatches = Hash::check($user->firstname, $inputUser['firstname']) && Hash::check($user->lastname, $inputUser['lastname']);

if ($emailMatches || $nameMatches) {
$matches += 1;
}

$sync = false;
if ($emailMatches && $nameMatches) {
$this->info("Found matching email address and name for {$user->firstname} {$user->lastname} ({$user->email}).");
$sync = $this->option('force') || $this->confirm("Synchronize UUID with file?");
} elseif ($emailMatches) {
$this->info("Found matching email address but different name for {$user->firstname} {$user->lastname} ({$user->email}).");
$sync = $this->option('force') || $this->confirm("Synchronize UUID with file?");
} elseif ($nameMatches) {
$this->info("Found matching name but different email address for {$user->firstname} {$user->lastname} ({$user->email}).");
$sync = $this->option('force') || $this->confirm("Synchronize UUID with file?");
}

if ($sync) {
$user->uuid = $inputUser['uuid'];
if (!$this->option('dry-run')) {
$user->save();
}
$this->warn("UUID synchronized for {$user->firstname} {$user->lastname} ({$user->email}).");
break;
}
}
}

if ($matches === 0) {
$this->info('No candidates found');
} else {
$this->info('Finished');
}
}

/**
* Generate the output that can be used as file to synchronize user UUIDs.
*/
protected function generateOutput()
{
$users = User::select('uuid', 'firstname', 'lastname', 'email')->get();
$bar = $this->output->createProgressBar($users->count());
$users =
$users->map(function ($user) use ($bar) {
$bar->advance();

return [
'uuid' => $user->uuid,
'firstname' => Hash::make($user->firstname),
'lastname' => Hash::make($user->lastname),
'email' => Hash::make($user->email),
];
})->toJson(JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$bar->finish();

echo $users;
}
}
4 changes: 4 additions & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ protected function schedule(Schedule $schedule): void
->withoutOverlapping()
->onOneServer();

$schedule->command('sync:prune')
->daily()
->onOneServer();

// Insert scheduled tasks here.
}

Expand Down
79 changes: 79 additions & 0 deletions app/Http/Controllers/Api/Export/Controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace Biigle\Http\Controllers\Api\Export;

use Biigle\Http\Controllers\Api\Controller as BaseController;
use Illuminate\Http\Request;

abstract class Controller extends BaseController
{
/**
* Creates a new instance.
*/
public function __construct()
{
$this->middleware('can:sudo');
}

/**
* Handle a generic export request.
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function show(Request $request)
{
if (!$this->isAllowed()) {
abort(404);
}

$this->validate($request, ['except' => 'filled', 'only' => 'filled']);
$query = $this->getQuery();

if ($request->filled('except')) {
$query = $query->whereNotIn('id', explode(',', $request->input('except')));
} elseif ($request->filled('only')) {
$query = $query->whereIn('id', explode(',', $request->input('only')));
}

$export = $this->getExport($query->pluck('id')->toArray());

return response()
->download($export->getArchive(), $this->getExportFilename(), [
'Content-Type' => 'application/zip',
])
->deleteFileAfterSend(true);
}

/**
* Get the query for the model to export.
*
* @return \Illuminate\Database\Query\Builder
*/
abstract protected function getQuery();

/**
* Get the new export instance.
*
* @param array $ids
* @return \Biigle\Services\Export\Export
*/
abstract protected function getExport(array $ids);

/**
* Get the filename of the export archive.
*
* @return string
*/
abstract protected function getExportFilename();

/**
* Determine if this kind of export is allowed by the config.
*
* @return bool
*/
protected function isAllowed()
{
return false;
}
}
52 changes: 52 additions & 0 deletions app/Http/Controllers/Api/Export/LabelTreeExportController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Biigle\Http\Controllers\Api\Export;

use Biigle\LabelTree;
use Biigle\Services\Export\LabelTreeExport;

class LabelTreeExportController extends Controller
{
/**
* @api {get} export/label-trees Get a label tree export
* @apiGroup Sync
* @apiName ShowLabelTreeExport
*
* @apiParam (Optional arguments) {String} except Comma separated IDs of the label trees that should not be included in the export file.
* @apiParam (Optional arguments) {String} only Comma separated IDs of the label trees that should only be included in the export file.
* @apiDescription The response is a ZIP archive that can be used for the label tree import. By default all label trees are exported.
* @apiPermission admin
*/

/**
* {@inheritdoc}
*/
protected function getQuery()
{
return LabelTree::getQuery();
}

/**
* {@inheritdoc}
*/
protected function getExport(array $ids)
{
return new LabelTreeExport($ids);
}

/**
* {@inheritdoc}
*/
protected function getExportFilename()
{
return 'biigle_label_tree_export.zip';
}

/**
* {@inheritdoc}
*/
protected function isAllowed()
{
return in_array('labelTrees', config('sync.allowed_exports'));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Biigle\Http\Controllers\Api\Export;

use Biigle\Http\Controllers\Api\Controller as BaseController;
use Biigle\Http\Requests\ShowPublicLabelTreeExport;
use Biigle\Services\Export\PublicLabelTreeExport;

class PublicLabelTreeExportController extends BaseController
{
/**
* Handle a public label tree export request.
*
* @api {get} public-export/label-trees/:id Download a label tree
* @apiGroup Sync
* @apiName ShowPublicLabelTreeExport
* @apiDescription The response is a ZIP archive that contains a JSON file with label tree attributes and a CSV file with label attributes.
*
* @apiParam {Number} id The label tree ID
*
* @apiPermission labelTreeMemberIfPrivate
*
* @param ShowPublicLabelTreeExport $request
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function show(ShowPublicLabelTreeExport $request)
{
$export = new PublicLabelTreeExport([$request->tree->id]);

return response()
->download($export->getArchive(), 'biigle_label_tree_export.zip', [
'Content-Type' => 'application/zip',
])
->deleteFileAfterSend(true);
}
}
52 changes: 52 additions & 0 deletions app/Http/Controllers/Api/Export/UserExportController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Biigle\Http\Controllers\Api\Export;

use Biigle\Services\Export\UserExport;
use Biigle\User;

class UserExportController extends Controller
{
/**
* @api {get} export/users Get a user export
* @apiGroup Sync
* @apiName ShowUserExport
*
* @apiParam (Optional arguments) {String} except Comma separated IDs of the users that should not be included in the export file.
* @apiParam (Optional arguments) {String} only Comma separated IDs of the users that should only be included in the export file.
* @apiDescription The response is a ZIP archive that can be used for the user import. By default all users are exported.
* @apiPermission admin
*/

/**
* {@inheritdoc}
*/
protected function getQuery()
{
return User::getQuery();
}

/**
* {@inheritdoc}
*/
protected function getExport(array $ids)
{
return new UserExport($ids);
}

/**
* {@inheritdoc}
*/
protected function getExportFilename()
{
return 'biigle_user_export.zip';
}

/**
* {@inheritdoc}
*/
protected function isAllowed()
{
return in_array('users', config('sync.allowed_exports'));
}
}
Loading

0 comments on commit 725930d

Please sign in to comment.