mirror of
https://github.com/pixelfed/pixelfed.git
synced 2025-01-10 14:10:46 +00:00
Update Data Export, refactor following/follower and statuses exports to allow accounts of any size with api entity instead of ap
This commit is contained in:
parent
6a20c34de1
commit
0d25917c29
2 changed files with 211 additions and 94 deletions
|
@ -2,25 +2,23 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Settings;
|
namespace App\Http\Controllers\Settings;
|
||||||
|
|
||||||
use App\AccountLog;
|
|
||||||
use App\Following;
|
|
||||||
use App\Report;
|
|
||||||
use App\Status;
|
use App\Status;
|
||||||
use App\UserFilter;
|
use App\Transformer\ActivityPub\ProfileTransformer;
|
||||||
use Auth, Cookie, DB, Cache, Purify;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use App\Transformer\ActivityPub\{
|
|
||||||
ProfileTransformer,
|
|
||||||
StatusTransformer
|
|
||||||
};
|
|
||||||
use App\Transformer\Api\StatusTransformer as StatusApiTransformer;
|
use App\Transformer\Api\StatusTransformer as StatusApiTransformer;
|
||||||
|
use App\UserFilter;
|
||||||
|
use Auth;
|
||||||
|
use Cache;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use League\Fractal;
|
use League\Fractal;
|
||||||
use League\Fractal\Serializer\ArraySerializer;
|
use League\Fractal\Serializer\ArraySerializer;
|
||||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
use Storage;
|
||||||
|
|
||||||
trait ExportSettings
|
trait ExportSettings
|
||||||
{
|
{
|
||||||
|
private const CHUNK_SIZE = 1000;
|
||||||
|
|
||||||
|
private const STORAGE_BASE = 'user_exports';
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->middleware('auth');
|
$this->middleware('auth');
|
||||||
|
@ -33,47 +31,146 @@ trait ExportSettings
|
||||||
|
|
||||||
public function exportAccount()
|
public function exportAccount()
|
||||||
{
|
{
|
||||||
$data = Cache::remember('account:export:profile:actor:'.Auth::user()->profile->id, now()->addMinutes(60), function() {
|
|
||||||
$profile = Auth::user()->profile;
|
$profile = Auth::user()->profile;
|
||||||
$fractal = new Fractal\Manager();
|
$fractal = new Fractal\Manager;
|
||||||
$fractal->setSerializer(new ArraySerializer());
|
$fractal->setSerializer(new ArraySerializer);
|
||||||
$resource = new Fractal\Resource\Item($profile, new ProfileTransformer());
|
$resource = new Fractal\Resource\Item($profile, new ProfileTransformer);
|
||||||
return $fractal->createData($resource)->toArray();
|
|
||||||
});
|
$data = $fractal->createData($resource)->toArray();
|
||||||
|
|
||||||
return response()->streamDownload(function () use ($data) {
|
return response()->streamDownload(function () use ($data) {
|
||||||
echo json_encode($data, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
|
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
}, 'account.json', [
|
}, 'account.json', [
|
||||||
'Content-Type' => 'application/json'
|
'Content-Type' => 'application/json',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function exportFollowing()
|
public function exportFollowing()
|
||||||
{
|
{
|
||||||
$data = Cache::remember('account:export:profile:following:'.Auth::user()->profile->id, now()->addMinutes(60), function() {
|
$profile = Auth::user()->profile;
|
||||||
return Auth::user()->profile->following()->get()->map(function($i) {
|
$userId = Auth::id();
|
||||||
return $i->url();
|
|
||||||
|
$userExportPath = 'user_exports/'.$userId;
|
||||||
|
$filename = 'pixelfed-following.json';
|
||||||
|
$tempPath = $userExportPath.'/'.$filename;
|
||||||
|
|
||||||
|
if (! Storage::exists($userExportPath)) {
|
||||||
|
Storage::makeDirectory($userExportPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Storage::put($tempPath, '[');
|
||||||
|
|
||||||
|
$profile->following()
|
||||||
|
->chunk(1000, function ($following) use ($tempPath) {
|
||||||
|
$urls = $following->map(function ($follow) {
|
||||||
|
return $follow->url();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$json = json_encode($urls,
|
||||||
|
JSON_PRETTY_PRINT |
|
||||||
|
JSON_UNESCAPED_SLASHES |
|
||||||
|
JSON_UNESCAPED_UNICODE
|
||||||
|
);
|
||||||
|
|
||||||
|
$json = trim($json, '[]');
|
||||||
|
if (Storage::size($tempPath) > 1) {
|
||||||
|
$json = ','.$json;
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage::append($tempPath, $json);
|
||||||
});
|
});
|
||||||
return response()->streamDownload(function () use($data) {
|
|
||||||
echo $data;
|
Storage::append($tempPath, ']');
|
||||||
}, 'following.json', [
|
|
||||||
'Content-Type' => 'application/json'
|
return response()->stream(
|
||||||
]);
|
function () use ($tempPath) {
|
||||||
|
$handle = fopen(Storage::path($tempPath), 'rb');
|
||||||
|
while (! feof($handle)) {
|
||||||
|
echo fread($handle, 8192);
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
Storage::delete($tempPath);
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Content-Disposition' => 'attachment; filename="pixelfed-following.json"',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if (Storage::exists($tempPath)) {
|
||||||
|
Storage::delete($tempPath);
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function exportFollowers()
|
public function exportFollowers()
|
||||||
{
|
{
|
||||||
$data = Cache::remember('account:export:profile:followers:'.Auth::user()->profile->id, now()->addMinutes(60), function() {
|
$profile = Auth::user()->profile;
|
||||||
return Auth::user()->profile->followers()->get()->map(function($i) {
|
$userId = Auth::id();
|
||||||
return $i->url();
|
|
||||||
|
$userExportPath = 'user_exports/'.$userId;
|
||||||
|
$filename = 'pixelfed-followers.json';
|
||||||
|
$tempPath = $userExportPath.'/'.$filename;
|
||||||
|
|
||||||
|
if (! Storage::exists($userExportPath)) {
|
||||||
|
Storage::makeDirectory($userExportPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Storage::put($tempPath, '[');
|
||||||
|
|
||||||
|
$profile->followers()
|
||||||
|
->chunk(1000, function ($followers) use ($tempPath) {
|
||||||
|
$urls = $followers->map(function ($follower) {
|
||||||
|
return $follower->url();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$json = json_encode($urls,
|
||||||
|
JSON_PRETTY_PRINT |
|
||||||
|
JSON_UNESCAPED_SLASHES |
|
||||||
|
JSON_UNESCAPED_UNICODE
|
||||||
|
);
|
||||||
|
|
||||||
|
$json = trim($json, '[]');
|
||||||
|
if (Storage::size($tempPath) > 1) {
|
||||||
|
$json = ','.$json;
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage::append($tempPath, $json);
|
||||||
});
|
});
|
||||||
return response()->streamDownload(function () use($data) {
|
|
||||||
echo $data;
|
Storage::append($tempPath, ']');
|
||||||
}, 'followers.json', [
|
|
||||||
'Content-Type' => 'application/json'
|
return response()->stream(
|
||||||
]);
|
function () use ($tempPath) {
|
||||||
|
$handle = fopen(Storage::path($tempPath), 'rb');
|
||||||
|
while (! feof($handle)) {
|
||||||
|
echo fread($handle, 8192);
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
Storage::delete($tempPath);
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Content-Disposition' => 'attachment; filename="pixelfed-followers.json"',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if (Storage::exists($tempPath)) {
|
||||||
|
Storage::delete($tempPath);
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function exportMuteBlockList()
|
public function exportMuteBlockList()
|
||||||
|
@ -82,63 +179,83 @@ trait ExportSettings
|
||||||
$exists = UserFilter::select('id')
|
$exists = UserFilter::select('id')
|
||||||
->whereUserId($profile->id)
|
->whereUserId($profile->id)
|
||||||
->exists();
|
->exists();
|
||||||
if(!$exists) {
|
if (! $exists) {
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
$data = Cache::remember('account:export:profile:muteblocklist:'.Auth::user()->profile->id, now()->addMinutes(60), function() use($profile) {
|
$data = Cache::remember('account:export:profile:muteblocklist:'.Auth::user()->profile->id, now()->addMinutes(60), function () use ($profile) {
|
||||||
return json_encode([
|
return json_encode([
|
||||||
'muted' => $profile->mutedProfileUrls(),
|
'muted' => $profile->mutedProfileUrls(),
|
||||||
'blocked' => $profile->blockedProfileUrls()
|
'blocked' => $profile->blockedProfileUrls(),
|
||||||
], JSON_PRETTY_PRINT);
|
], JSON_PRETTY_PRINT);
|
||||||
});
|
});
|
||||||
return response()->streamDownload(function () use($data) {
|
|
||||||
|
return response()->streamDownload(function () use ($data) {
|
||||||
echo $data;
|
echo $data;
|
||||||
}, 'muted-and-blocked-accounts.json', [
|
}, 'muted-and-blocked-accounts.json', [
|
||||||
'Content-Type' => 'application/json'
|
'Content-Type' => 'application/json',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function exportStatuses(Request $request)
|
public function exportStatuses(Request $request)
|
||||||
{
|
{
|
||||||
$this->validate($request, [
|
|
||||||
'type' => 'required|string|in:ap,api'
|
|
||||||
]);
|
|
||||||
$limit = 500;
|
|
||||||
|
|
||||||
$profile = Auth::user()->profile;
|
$profile = Auth::user()->profile;
|
||||||
$type = 'ap';
|
$userId = Auth::id();
|
||||||
|
$userExportPath = self::STORAGE_BASE.'/'.$userId;
|
||||||
|
$filename = 'pixelfed-statuses.json';
|
||||||
|
$tempPath = $userExportPath.'/'.$filename;
|
||||||
|
|
||||||
$count = Status::select('id')->whereProfileId($profile->id)->count();
|
if (! Storage::exists($userExportPath)) {
|
||||||
if($count > $limit) {
|
Storage::makeDirectory($userExportPath);
|
||||||
// fire background job
|
|
||||||
return redirect('/settings/data-export')->with(['status' => 'You have more than '.$limit.' statuses, we do not support full account export yet.']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$filename = 'outbox.json';
|
Storage::put($tempPath, '[');
|
||||||
if($type == 'ap') {
|
$fractal = new Fractal\Manager;
|
||||||
$data = Cache::remember('account:export:profile:statuses:ap:'.Auth::user()->profile->id, now()->addHours(1), function() {
|
$fractal->setSerializer(new ArraySerializer);
|
||||||
$profile = Auth::user()->profile->statuses;
|
|
||||||
$fractal = new Fractal\Manager();
|
try {
|
||||||
$fractal->setSerializer(new ArraySerializer());
|
Status::whereProfileId($profile->id)
|
||||||
$resource = new Fractal\Resource\Collection($profile, new StatusTransformer());
|
->chunk(self::CHUNK_SIZE, function ($statuses) use ($fractal, $tempPath) {
|
||||||
return $fractal->createData($resource)->toArray();
|
$resource = new Fractal\Resource\Collection($statuses, new StatusApiTransformer);
|
||||||
|
$data = $fractal->createData($resource)->toArray();
|
||||||
|
|
||||||
|
$json = json_encode($data,
|
||||||
|
JSON_PRETTY_PRINT |
|
||||||
|
JSON_UNESCAPED_SLASHES |
|
||||||
|
JSON_UNESCAPED_UNICODE
|
||||||
|
);
|
||||||
|
|
||||||
|
$json = trim($json, '[]');
|
||||||
|
if (Storage::size($tempPath) > 1) {
|
||||||
|
$json = ','.$json;
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage::append($tempPath, $json);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
$filename = 'api-statuses.json';
|
|
||||||
$data = Cache::remember('account:export:profile:statuses:api:'.Auth::user()->profile->id, now()->addHours(1), function() {
|
|
||||||
$profile = Auth::user()->profile->statuses;
|
|
||||||
$fractal = new Fractal\Manager();
|
|
||||||
$fractal->setSerializer(new ArraySerializer());
|
|
||||||
$resource = new Fractal\Resource\Collection($profile, new StatusApiTransformer());
|
|
||||||
return $fractal->createData($resource)->toArray();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->streamDownload(function () use ($data, $filename) {
|
Storage::append($tempPath, ']');
|
||||||
echo json_encode($data, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
|
|
||||||
}, $filename, [
|
|
||||||
'Content-Type' => 'application/json'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return response()->stream(
|
||||||
|
function () use ($tempPath) {
|
||||||
|
$handle = fopen(Storage::path($tempPath), 'rb');
|
||||||
|
while (! feof($handle)) {
|
||||||
|
echo fread($handle, 8192);
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
Storage::delete($tempPath);
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Content-Disposition' => 'attachment; filename="pixelfed-statuses.json"',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if (Storage::exists($tempPath)) {
|
||||||
|
Storage::delete($tempPath);
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -25,7 +25,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
||||||
$pid = request()->user()->profile_id;
|
$pid = request()->user()->profile_id;
|
||||||
$taggedPeople = MediaTagService::get($status->id);
|
$taggedPeople = MediaTagService::get($status->id);
|
||||||
$poll = $status->type === 'poll' ? PollService::get($status->id, $pid) : null;
|
$poll = $status->type === 'poll' ? PollService::get($status->id, $pid) : null;
|
||||||
$content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : "";
|
$content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : '';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'_v' => 1,
|
'_v' => 1,
|
||||||
|
@ -33,8 +33,8 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
||||||
'shortcode' => HashidService::encode($status->id),
|
'shortcode' => HashidService::encode($status->id),
|
||||||
'uri' => $status->url(),
|
'uri' => $status->url(),
|
||||||
'url' => $status->url(),
|
'url' => $status->url(),
|
||||||
'in_reply_to_id' => (string) $status->in_reply_to_id,
|
'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null,
|
||||||
'in_reply_to_account_id' => (string) $status->in_reply_to_profile_id,
|
'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null,
|
||||||
'reblog' => $status->reblog_of_id ? StatusService::get($status->reblog_of_id) : null,
|
'reblog' => $status->reblog_of_id ? StatusService::get($status->reblog_of_id) : null,
|
||||||
'content' => $content,
|
'content' => $content,
|
||||||
'content_text' => $status->caption,
|
'content_text' => $status->caption,
|
||||||
|
|
Loading…
Reference in a new issue