FEATURE: File Backup

Turned on file backups that grabs uploaded images in the public
directory and saves them in a zip so they can be downloaded and
archived.
This commit is contained in:
ro 2024-07-03 14:30:32 -06:00
parent bc7b1fe7ec
commit 4e880092c1
No known key found for this signature in database
GPG key ID: 29B551CDBD4D3B50
8 changed files with 144 additions and 46 deletions

View file

@ -58,15 +58,36 @@ class SettingsAPIController extends Controller
public function createBackup(Request $request) public function createBackup(Request $request)
{ {
return response()->json($this->maintenance->createBackup())->header('Content-Type', 'application/json'); $body = json_decode($request->getContent());
if ($body->task == 'content_backup') {
return response()->json(
$this->maintenance->createContentBackUp()
)->header('Content-Type', 'application/json');
} else {
return response()->json(
$this->maintenance->createFileBackUp()
)->header('Content-Type', 'application/json');
}
} }
public function downloadBackup(Request $request) public function downloadBackup(Request $request)
{ {
if ($this->member::status()) { if ($this->member::status()) {
$latest = $this->settings->getGlobal()['last_backup']; $latest = '';
$file = 'backup-' . $latest . '.zip'; $file = '';
return response()->download('../content/backups/' . $file, $file, ['Content-Type: application/zip']); if (explode('/', $request->getRequestUri())[4] == 'content-download') {
$latest = $this->settings->getGlobal()['last_content_backup'];
$file = 'backup-content-' . $latest . '.zip';
} else {
$latest = $this->settings->getGlobal()['last_files_backup'];
$file = 'backup-files-' . $latest . '.zip';
}
return response()->download(
'../content/backups/' . $file,
$file,
['Content-Type: application/zip']
);
} }
} }

View file

@ -266,10 +266,11 @@ class SortingService
public function settings() public function settings()
{ {
$global = $this->settings->getGlobal(); $global = $this->settings->getGlobal();
$updated = new Carbon($global['last_backup']); $updatedContent = new Carbon($global['last_content_backup']);
$status = session('member') != '' ? true : false; $updatedFiles = new Carbon($global['last_files_backup']);
$pageOptions = [ $status = session('member') != '' ? true : false;
$pageOptions = [
'title' => 'Settings', 'title' => 'Settings',
'private' => $global['private'], 'private' => $global['private'],
'renderOnSave' => $global['renderOnSave'], 'renderOnSave' => $global['renderOnSave'],
@ -279,7 +280,8 @@ class SortingService
'siteTitle' => $global['title'], 'siteTitle' => $global['title'],
'baseUrl' => $global['base_url'], 'baseUrl' => $global['base_url'],
'desc' => $global['descriptions'], 'desc' => $global['descriptions'],
'lastBackup' => $updated->format('Y F d H i s'), 'lastContentBackup' => $updatedContent->format('Y F d H i s'),
'lastFilesBackup' => $updatedFiles->format('Y F d H i s'),
'currentTheme' => $global['theme'], 'currentTheme' => $global['theme'],
'themes' => $this->themes->getThemes(), 'themes' => $this->themes->getThemes(),
'apiStatus' => isset($global['externalAPI']) ? $global['externalAPI'] : 'false', 'apiStatus' => isset($global['externalAPI']) ? $global['externalAPI'] : 'false',

View file

@ -14,7 +14,7 @@ class MaintenanceService
$this->settings = $settingsService; $this->settings = $settingsService;
} }
public function createBackUp() public function createContentBackUp()
{ {
//make sure back directory is there //make sure back directory is there
$stamp = Carbon::now()->format("YmdGis"); $stamp = Carbon::now()->format("YmdGis");
@ -25,7 +25,7 @@ class MaintenanceService
$zip = new \ZipArchive(); $zip = new \ZipArchive();
$zip->open( $zip->open(
env('FIPAMO_BACKUPS') . '/backup-' . $stamp . '.zip', env('FIPAMO_BACKUPS') . '/backup-content-' . $stamp . '.zip',
\ZipArchive::CREATE | \ZipArchive::OVERWRITE \ZipArchive::CREATE | \ZipArchive::OVERWRITE
); );
//gather data and path info for md pages //gather data and path info for md pages
@ -63,29 +63,36 @@ class MaintenanceService
//gather paths for user images //gather paths for user images
$userImages = []; $userImages = [];
$dir = new \RecursiveDirectoryIterator('../public/assets/images/user'); if (is_dir('../public/assets/images/user')) {
$flat = new \RecursiveIteratorIterator($dir); $dir = new \RecursiveDirectoryIterator('../public/assets/images/user');
$files = new \RegexIterator($flat, '/\.png|jpg|gif$/i'); $flat = new \RecursiveIteratorIterator($dir);
foreach ($files as $file) { $files = new \RegexIterator($flat, '/\.png|jpg|gif$/i');
$userImages[] = ['path' => $file->getPath(), 'file' => $file->getFilename()]; foreach ($files as $file) {
}; $userImages[] = ['path' => $file->getPath(), 'file' => $file->getFilename()];
};
}
//gather paths for blog documents //gather paths for blog documents
$blogDocs = []; $blogDocs = [];
$dir = new \RecursiveDirectoryIterator('../public/assets/docs/blog'); if (is_dir('../public/assets/docs/blog')) {
$flat = new \RecursiveIteratorIterator($dir); $dir = new \RecursiveDirectoryIterator('../public/assets/docs/blog');
$files = new \RegexIterator($flat, '/\.txt|pdf|rtf$/i'); $flat = new \RecursiveIteratorIterator($dir);
foreach ($files as $file) { $files = new \RegexIterator($flat, '/\.txt|pdf|rtf$/i');
$blogDocs[] = ['path' => $file->getPath(), 'file' => $file->getFilename()]; foreach ($files as $file) {
}; $blogDocs[] = ['path' => $file->getPath(), 'file' => $file->getFilename()];
};
}
//gather paths for blog videos //gather paths for blog videos
$blogVids = []; $blogVids = [];
$dir = new \RecursiveDirectoryIterator('../public/assets/video/blog'); if (is_dir('../public/assets/video/blog')) {
$flat = new \RecursiveIteratorIterator($dir); $dir = new \RecursiveDirectoryIterator('../public/assets/video/blog');
$files = new \RegexIterator($flat, '/\.mp4$/i'); $flat = new \RecursiveIteratorIterator($dir);
foreach ($files as $file) { $files = new \RegexIterator($flat, '/\.mp4$/i');
$blogVids[] = ['path' => $file->getPath(), 'file' => $file->getFilename()]; foreach ($files as $file) {
}; $blogVids[] = ['path' => $file->getPath(), 'file' => $file->getFilename()];
};
}
//add directory for settings and save them //add directory for settings and save them
$zip->addEmptyDir('config'); $zip->addEmptyDir('config');
@ -112,8 +119,55 @@ class MaintenanceService
unlink(env('FIPAMO_BACKUPS') . '/blog_vids_temp.json'); unlink(env('FIPAMO_BACKUPS') . '/blog_vids_temp.json');
//update settings file with latest back up date //update settings file with latest back up date
$this->settings->updateGlobalData('last_backup', $stamp); $this->settings->updateGlobalData('last_content_backup', $stamp);
return ['message' => "Backup created. THIS IS A SAFE SPACE!"]; return ['message' => "Content backup created. THIS IS A SAFE SPACE!"];
}
public function createFileBackUp()
{
$stamp = Carbon::now()->format("YmdGis");
$zip = new \ZipArchive();
$zip->open(
env('FIPAMO_BACKUPS') . '/backup-files-' . $stamp . '.zip',
\ZipArchive::CREATE | \ZipArchive::OVERWRITE
);
//gather data and path info for blog images
$blogImagesPath = '../public/assets/images/blog';
$yearPaths = glob($blogImagesPath . '/*', GLOB_ONLYDIR);
foreach ($yearPaths as $years) {
$year = explode('/', $years);
$monthsPath = glob($blogImagesPath . '/' . $year[5] . '/*', GLOB_ONLYDIR);
foreach ($monthsPath as $months) {
$month = explode('/', $months);
//once info is collected, add images pages to zip
$options = [
'add_path' => 'public/assets/images/blog/' . $year[5] . '/' . $month[6] . '/',
'remove_all_path' => true,
];
$zip->addGlob($months . '/*.*', GLOB_BRACE, $options);
}
}
//gather data and path info for user images
$userImagesPath = '../public/assets/images/user';
$yearPaths = glob($userImagesPath . '/*', GLOB_ONLYDIR);
foreach ($yearPaths as $years) {
$year = explode('/', $years);
$monthsPath = glob($userImagesPath . '/' . $year[5] . '/*', GLOB_ONLYDIR);
foreach ($monthsPath as $months) {
$month = explode('/', $months);
//once info is collected, add images pages to zip
$options = [
'add_path' => 'public/assets/images/user/' . $year[5] . '/' . $month[6] . '/',
'remove_all_path' => true,
];
$zip->addGlob($months . '/*.*', GLOB_BRACE, $options);
}
}
$zip->close();
$this->settings->updateGlobalData('last_files_backup', $stamp);
return ['message' => "Files are backed up. Breath Easy!"];
} }
} }

View file

@ -8,7 +8,8 @@
"renderOnSave": "false", "renderOnSave": "false",
"theme": "fipamo-default-v2", "theme": "fipamo-default-v2",
"display_limit": 5, "display_limit": 5,
"last_backup": null, "last_content_backup": null,
"last_file_backup": null,
"externalAPI": "false", "externalAPI": "false",
"dynamicRender": "false" "dynamicRender": "false"
}, },

View file

@ -151,7 +151,7 @@ class MaintenanceManager {
* Promise method for creating a zip back up of current site. For local use only. * Promise method for creating a zip back up of current site. For local use only.
*/ */
backup() { backup(backup_task) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var url, event, method, type, data; var url, event, method, type, data;
@ -159,7 +159,7 @@ class MaintenanceManager {
event = TASK_BACKUP_CREATE; event = TASK_BACKUP_CREATE;
method = REQUEST_TYPE_PUT; method = REQUEST_TYPE_PUT;
type = CONTENT_TYPE_JSON; type = CONTENT_TYPE_JSON;
data = { task: 'create_backup' }; data = backup_task;
this._request(url, null, event, method, type, data) this._request(url, null, event, method, type, data)
.then(result => { .then(result => {
resolve(result); resolve(result);

View file

@ -156,7 +156,11 @@ export default class SettingsIndex {
} }
//handle backup from settings //handle backup from settings
document document
.getElementById('create-backup') .getElementById('create-content-backup')
.addEventListener('click', e => this.handleBackup(e));
document
.getElementById('create-file-backup')
.addEventListener('click', e => this.handleBackup(e)); .addEventListener('click', e => this.handleBackup(e));
} }
//-------------------------- //--------------------------
@ -259,9 +263,19 @@ export default class SettingsIndex {
handleBackup(e) { handleBackup(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
notify.alert('Creating backup', null); let id = '';
let type = '';
e.target.id == '' ? (id = e.target.parentNode.id) : (id = e.target.id);
if (id == 'create-content-backup') {
notify.alert('Creating Content Backup', null);
type = { task: 'content_backup' };
} else {
notify.alert('Creating File Backup', null);
type = { task: 'file_backup' };
}
this.mm this.mm
.backup() .backup(type)
.then(r => { .then(r => {
notify.alert(r.message, true); notify.alert(r.message, true);
}) })

View file

@ -94,19 +94,18 @@
<svg id="nav-menu-icon" class="icon"> <svg id="nav-menu-icon" class="icon">
<use id="nav-menu-icon" xlink:href="/assets/images/global/sprite.svg#entypo-copy"/> <use id="nav-menu-icon" xlink:href="/assets/images/global/sprite.svg#entypo-copy"/>
</svg> </svg>
<button id="create-backup"> <button id="create-content-backup">
<span>CONTENT BACKUP</span> <span>CONTENT BACKUP</span>
</button> </button>
<span> <span>
@if($lastBackup != '') @if($lastContentBackup != '')
LAST BACK UP MOST RECENT:
<a href="/api/v1/backup/download">{{ $lastBackup }}</a><br/> <a href="/api/v1/backup/content-download">{{ $lastContentBackup }}</a><br/>
@else @else
<span>span No back ups. Frowny face.</span> <span>span No back ups. Frowny face.</span>
@endif @endif
<span> <span>
</div> </div>
<!-- TODO: File Back up option
<div class="option-container"> <div class="option-container">
<svg id="nav-menu-icon" class="icon"> <svg id="nav-menu-icon" class="icon">
<use id="nav-menu-icon" xlink:href="/assets/images/global/sprite.svg#entypo-images"/> <use id="nav-menu-icon" xlink:href="/assets/images/global/sprite.svg#entypo-images"/>
@ -114,9 +113,15 @@
<button id="create-file-backup"> <button id="create-file-backup">
<span>FILE BACKUP</span> <span>FILE BACKUP</span>
</button> </button>
<span>COMING SOON</span> <span>
@if($lastFilesBackup != '')
MOST RECENT:
<a href="/api/v1/backup/files-download">{{ $lastFilesBackup }}</a><br/>
@else
<span>span No back ups. Frowny face.</span>
@endif
</span>
</div> </div>
-->
<div class="option-container"> <div class="option-container">
<svg id="nav-menu-icon" class="icon"> <svg id="nav-menu-icon" class="icon">
<use id="nav-menu-icon" xlink:href="/assets/images/global/sprite.svg#entypo-back-in-time"/> <use id="nav-menu-icon" xlink:href="/assets/images/global/sprite.svg#entypo-back-in-time"/>
@ -124,7 +129,7 @@
<button id="reset-to-default"> <button id="reset-to-default">
<span>RESET TO DEFAULT</span> <span>RESET TO DEFAULT</span>
</button> </button>
<span>Restores site to default state. !CANNOT UNDO!</span> <span>Deletes all content and configs <strong>CANNOT UNDO</strong></span>
</div> </div>
</div> </div>
</section> </section>

View file

@ -31,7 +31,8 @@ Route::put("/v1/settings/publish", [SettingsAPIController::class, 'publish']);
Route::put("/v1/settings/sync", [SettingsAPIController::class, 'sync']); Route::put("/v1/settings/sync", [SettingsAPIController::class, 'sync']);
Route::put("/v1/settings/nav-sync", [SettingsAPIController::class, 'navSync']); Route::put("/v1/settings/nav-sync", [SettingsAPIController::class, 'navSync']);
Route::put("/v1/backup/create", [SettingsAPIController::class, 'createBackup']); Route::put("/v1/backup/create", [SettingsAPIController::class, 'createBackup']);
Route::get("/v1/backup/download", [SettingsAPIController::class, 'downloadBackup']); Route::get("/v1/backup/content-download", [SettingsAPIController::class, 'downloadBackup']);
Route::get("/v1/backup/files-download", [SettingsAPIController::class, 'downloadBackup']);
//init //init
Route::post("/v1/init", [InitAPIController::class, 'setupFresh']); Route::post("/v1/init", [InitAPIController::class, 'setupFresh']);
Route::post("/v1/restore", [InitAPIController::class, 'setupRestore']); Route::post("/v1/restore", [InitAPIController::class, 'setupRestore']);