✅ Fixed bulk conversion getting stuck on missing files ✅ Added robust error handling and timeout protection ✅ Improved JavaScript response parsing ✅ Added file existence validation ✅ Fixed missing PHP class imports ✅ Added comprehensive try-catch error recovery 🔧 Key fixes: - File existence checks before conversion attempts - 30-second timeout protection per file - Graceful handling of 500 errors and JSON parsing issues - Automatic continuation to next file on failures - Cache busting for JavaScript updates 🎯 Result: Bulk conversion now completes successfully even with missing files 🚀 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
660 lines
21 KiB
PHP
660 lines
21 KiB
PHP
<?php
|
|
|
|
namespace WebPExpress;
|
|
|
|
use \WebPConvert\Convert\Converters\Stack;
|
|
use \WebPConvert\WebPConvert;
|
|
use \ImageMimeTypeGuesser\ImageMimeTypeGuesser;
|
|
|
|
/**
|
|
*
|
|
*/
|
|
|
|
class WCFMApi
|
|
{
|
|
private static function doProcessRequest() {
|
|
if (!check_ajax_referer('webpexpress-wcfm-nonce', 'nonce', false)) {
|
|
throw new \Exception('The security nonce has expired. You need to reload (press F5) and try again)');
|
|
}
|
|
Validate::postHasKey('command');
|
|
$command = sanitize_text_field(stripslashes($_POST['command']));
|
|
|
|
switch ($command) {
|
|
/*
|
|
case 'get-tree':
|
|
$result = self::processGetTree();
|
|
break;*/
|
|
case 'get-folder':
|
|
$result = self::processGetFolder();
|
|
break;
|
|
case 'conversion-settings':
|
|
$result = self::processConversionSettings();
|
|
break;
|
|
case 'info':
|
|
$result = self::processInfo();
|
|
break;
|
|
case 'convert':
|
|
$result = self::processConvert();
|
|
break;
|
|
case 'delete-converted':
|
|
$result = self::processDeleteConverted();
|
|
break;
|
|
default:
|
|
throw new \Exception('Unknown command');
|
|
}
|
|
if (!isset($result)) {
|
|
throw new \Exception('Command: ' . $command . ' gave no result');
|
|
}
|
|
|
|
$json = wp_json_encode($result, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
if ($json === false) {
|
|
// TODO: We can do better error handling than this!
|
|
throw new \Exception('Failed encoding result to JSON');
|
|
} else {
|
|
echo $json;
|
|
}
|
|
wp_die();
|
|
}
|
|
|
|
public static function processRequest() {
|
|
try {
|
|
self::doProcessRequest();
|
|
}
|
|
catch (\Exception $e) {
|
|
wp_send_json_error($e->getMessage());
|
|
wp_die();
|
|
}
|
|
}
|
|
/*
|
|
{
|
|
"converters": [
|
|
{
|
|
"converter": "cwebp",
|
|
"options": {
|
|
"use-nice": true,
|
|
"try-common-system-paths": true,
|
|
"try-supplied-binary-for-os": true,
|
|
"method": 6,
|
|
"low-memory": true,
|
|
"command-line-options": ""
|
|
},
|
|
"working": true
|
|
},
|
|
{
|
|
"converter": "vips",
|
|
"options": {
|
|
"smart-subsample": false,
|
|
"preset": "none"
|
|
},
|
|
"working": false
|
|
},
|
|
{
|
|
"converter": "imagemagick",
|
|
"options": {
|
|
"use-nice": true
|
|
},
|
|
"working": true,
|
|
"deactivated": true
|
|
},
|
|
{
|
|
"converter": "graphicsmagick",
|
|
"options": {
|
|
"use-nice": true
|
|
},
|
|
"working": false
|
|
},
|
|
{
|
|
"converter": "ffmpeg",
|
|
"options": {
|
|
"use-nice": true,
|
|
"method": 4
|
|
},
|
|
"working": false
|
|
},
|
|
{
|
|
"converter": "wpc",
|
|
"working": false,
|
|
"options": {
|
|
"api-key": ""
|
|
}
|
|
},
|
|
{
|
|
"converter": "ewww",
|
|
"working": false
|
|
},
|
|
{
|
|
"converter": "imagick",
|
|
"working": false
|
|
},
|
|
{
|
|
"converter": "gmagick",
|
|
"working": false
|
|
},
|
|
{
|
|
"converter": "gd",
|
|
"options": {
|
|
"skip-pngs": false
|
|
},
|
|
"working": false
|
|
}
|
|
]
|
|
}*/
|
|
public static function processConversionSettings() {
|
|
require_once __DIR__ . "/../../vendor/autoload.php";
|
|
$availableConverters = Stack::getAvailableConverters();
|
|
|
|
/*
|
|
$converters = [];
|
|
//$supportsEncoding = [];
|
|
foreach ($availableConverters as $converter) {
|
|
$converters[] = [
|
|
'id' => $converter,
|
|
'name' => $converter
|
|
];
|
|
/*if () {
|
|
$supportsEncoding[] = $converter;
|
|
}*/
|
|
//}
|
|
|
|
$webpConvertOptionDefinitions = WebPConvert::getConverterOptionDefinitions();
|
|
|
|
$config = Config::loadConfigAndFix();
|
|
$defaults = [
|
|
'auto-limit' => (isset($config['quality-auto']) && $config['quality-auto']),
|
|
'alpha-quality' => $config['alpha-quality'],
|
|
'quality' => $config['max-quality'],
|
|
'encoding' => $config['jpeg-encoding'],
|
|
'near-lossless' => ($config['jpeg-enable-near-lossless'] ? $config['jpeg-near-lossless'] : 100),
|
|
'metadata' => $config['metadata'],
|
|
'stack-converters' => ConvertersHelper::getActiveConverterIds($config),
|
|
|
|
// 'method' (I could copy from cwebp...)
|
|
// 'sharp-yuv' (n/a)
|
|
// low-memory (n/a)
|
|
// auto-filter (n/a)
|
|
// preset (n/a)
|
|
// size-in-percentage (I could copy from cwebp...)
|
|
];
|
|
|
|
$good = ConvertersHelper::getWorkingAndActiveConverterIds($config);
|
|
if (isset($good[0])) {
|
|
$defaults['converter'] = $good[0];
|
|
}
|
|
//'converter' => 'ewww',
|
|
|
|
|
|
// TODO:add PNG options
|
|
$pngDefaults = [
|
|
'encoding' => $config['png-encoding'],
|
|
'near-lossless' => ($config['png-enable-near-lossless'] ? $config['png-near-lossless'] : 100),
|
|
'quality' => $config['png-quality'],
|
|
];
|
|
|
|
|
|
// Filter active converters
|
|
foreach ($config['converters'] as $converter) {
|
|
/*if (isset($converter['deactivated']) && ($converter['deactivated'])) {
|
|
//continue;
|
|
}*/
|
|
if (isset($converter['options'])) {
|
|
foreach ($converter['options'] as $optionName => $optionValue) {
|
|
$defaults[$converter['converter'] . '-' . $optionName] = $optionValue;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
$systemStatus = [
|
|
'converterRequirements' => [
|
|
'gd' => [
|
|
'extensionLoaded' => extension_loaded('gd'),
|
|
'compiledWithWebP' => function_exists('imagewebp'),
|
|
]
|
|
// TODO: Add more!
|
|
]
|
|
];
|
|
|
|
//getUnsupportedDefaultOptions
|
|
//supportedStandardOptions: {
|
|
$defaults['png'] = $pngDefaults;
|
|
|
|
return [
|
|
//'converters' => $converters,
|
|
'defaults' => $defaults,
|
|
//'pngDefaults' => $pngDefaults,
|
|
'options' => $webpConvertOptionDefinitions,
|
|
'systemStatus' => $systemStatus
|
|
];
|
|
|
|
/*
|
|
$config = Config::loadConfigAndFix();
|
|
// 'working', 'deactivated'
|
|
$foundFirstWorkingAndActive = false;
|
|
foreach ($config['converters'] as $converter) {
|
|
$converters[] = [
|
|
'id' => $converter['converter'],
|
|
'name' => $converter['converter']
|
|
];
|
|
if ($converter['working']) {
|
|
if
|
|
}
|
|
if (!$foundFirstWorkingAndActive) {
|
|
|
|
}
|
|
}*/
|
|
|
|
return [
|
|
'converters' => $converters
|
|
];
|
|
}
|
|
|
|
/*
|
|
* Get mime
|
|
* @return string
|
|
*/
|
|
private static function setMime($path, &$info) {
|
|
require_once __DIR__ . "/../../vendor/autoload.php";
|
|
$mimeResult = ImageMimeTypeGuesser::detect($path);
|
|
if (!$mimeResult) {
|
|
return;
|
|
}
|
|
$info['mime'] = $mimeResult;
|
|
if ($mimeResult == 'image/webp') {
|
|
$handle = @fopen($path, 'r');
|
|
if ($handle !== false) {
|
|
// 20 bytes is sufficient for all our sniffers, except image/svg+xml.
|
|
// The svg sniffer takes care of reading more
|
|
$sampleBin = @fread($handle, 20);
|
|
if ($sampleBin !== false) {
|
|
if (preg_match("/^RIFF.{4}WEBPVP8\ /", $sampleBin) === 1) {
|
|
$info['mime'] .= ' (lossy)';
|
|
} else if (preg_match("/^RIFF.{4}WEBPVP8L/", $sampleBin) === 1) {
|
|
$info['mime'] .= ' (lossless)';
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
public static function processInfo() {
|
|
|
|
Validate::postHasKey('args');
|
|
|
|
//$args = json_decode(sanitize_text_field(stripslashes($_POST['args'])), true);
|
|
|
|
//$args = $_POST['args'];
|
|
$args = self::getArgs();
|
|
if (!array_key_exists('path', $args)) {
|
|
throw new \Exception('"path" argument missing for command');
|
|
}
|
|
|
|
$path = SanityCheck::pathWithoutDirectoryTraversal($args['path']);
|
|
$path = ltrim($path, '/');
|
|
$pathTokens = explode('/', $path);
|
|
|
|
$rootId = array_shift($pathTokens); // Shift off the first item, which is the scope
|
|
$relPath = implode('/', $pathTokens);
|
|
$config = Config::loadConfigAndFix();
|
|
/*$rootIds = Paths::filterOutSubRoots($config['scope']);
|
|
if (!in_array($rootId, $rootIds)) {
|
|
throw new \Exception('Invalid scope (have you perhaps changed the scope setting after igniting the file manager?)');
|
|
}*/
|
|
$rootIds = $rootIds = Paths::getImageRootIds();
|
|
|
|
$absPath = Paths::getAbsDirById($rootId) . '/' . $relPath;
|
|
//absPathExistsAndIsFile
|
|
SanityCheck::absPathExists($absPath);
|
|
|
|
$result = [
|
|
'original' => [
|
|
//'filename' => $absPath,
|
|
//'abspath' => $absPath,
|
|
'size' => filesize($absPath),
|
|
// PS: I keep "&original" because some might have set up Nginx rules for ?original
|
|
'url' => Paths::getUrlById($rootId) . '/' . $relPath . '?' . SelfTestHelper::randomDigitsAndLetters(8) . '&dontreplace&original',
|
|
]
|
|
];
|
|
self::setMime($absPath, $result['original']);
|
|
|
|
// TODO: NO!
|
|
// We must use ConvertHelper::getDestination for the abs path.
|
|
// And we must use logic from AlterHtmlHelper to get the URL
|
|
//error_log('path:' . $absPathDest);
|
|
|
|
$destinationOptions = DestinationOptions::createFromConfig($config);
|
|
if ($destinationOptions->useDocRoot) {
|
|
if (!(Paths::canUseDocRootForStructuringCacheDir())) {
|
|
$destinationOptions->useDocRoot = false;
|
|
}
|
|
}
|
|
$imageRoots = new ImageRoots(Paths::getImageRootsDef());
|
|
$destinationPath = Paths::getDestinationPathCorrespondingToSource($absPath, $destinationOptions);
|
|
list($rootId, $destRelPath) = Paths::getRootAndRelPathForDestination($destinationPath, $imageRoots);
|
|
if ($rootId != '') {
|
|
$absPathDest = Paths::getAbsDirById($rootId) . '/' . $destRelPath;
|
|
$destinationUrl = Paths::getUrlById($rootId) . '/' . $destRelPath;
|
|
|
|
SanityCheck::absPath($absPathDest);
|
|
|
|
if (@file_exists($absPathDest)) {
|
|
$result['converted'] = [
|
|
//'abspath' => $absPathDest,
|
|
'size' => filesize($absPathDest),
|
|
'url' => $destinationUrl . '?' . SelfTestHelper::randomDigitsAndLetters(8),
|
|
];
|
|
self::setMime($absPathDest, $result['converted']);
|
|
}
|
|
|
|
// Get log, if exists. Ignore errors.
|
|
$log = '';
|
|
try {
|
|
$logFile = ConvertHelperIndependent::getLogFilename($absPath, Paths::getLogDirAbs());
|
|
if (@file_exists($logFile)) {
|
|
$logContent = file_get_contents($logFile);
|
|
if ($log !== false) {
|
|
$log = $logContent;
|
|
}
|
|
}
|
|
}
|
|
catch (\Exception $e) {
|
|
//throw $e;
|
|
}
|
|
|
|
$result['log'] = $log;
|
|
}
|
|
|
|
|
|
//$destinationUrl = DestinationUrl::
|
|
|
|
/*
|
|
error_log('dest:' . $destinationPath);
|
|
error_log('dest root:' . $rootId);
|
|
error_log('dest path:' . $destRelPath);
|
|
error_log('dest abs-dir:' . Paths::getAbsDirById($rootId) . '/' . $destRelPath);
|
|
error_log('dest url:' . Paths::getUrlById($rootId) . '/' . $destRelPath);
|
|
*/
|
|
|
|
//error_log('url:' . $destinationPath);
|
|
//error_log('destinationOptions' . print_r($destinationOptions, true));
|
|
|
|
/*
|
|
$destination = Paths::destinationPathConvenience($rootId, $relPath, $config);
|
|
$absPathDest = $destination['abs-path'];
|
|
SanityCheck::absPath($absPathDest);
|
|
error_log('path:' . $absPathDest);
|
|
|
|
if (@file_exists($absPathDest)) {
|
|
$result['converted'] = [
|
|
'abspath' => $destination['abs-path'],
|
|
'size' => filesize($destination['abs-path']),
|
|
'url' => $destination['url'],
|
|
'log' => ''
|
|
];
|
|
}
|
|
*/
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Translate path received (ie "/uploads/2021/...") to absolute path.
|
|
*
|
|
* @param string $path
|
|
*
|
|
* @return array [$absPath, $relPath, $rootId]
|
|
* @throws \Exception if root id is invalid or path doesn't pass sanity check
|
|
*/
|
|
private static function analyzePathReceived($path) {
|
|
try {
|
|
$path = SanityCheck::pathWithoutDirectoryTraversal($path);
|
|
$path = ltrim($path, '/');
|
|
$pathTokens = explode('/', $path);
|
|
|
|
$rootId = array_shift($pathTokens);
|
|
$relPath = implode('/', $pathTokens);
|
|
|
|
$rootIds = Paths::getImageRootIds();
|
|
if (!in_array($rootId, $rootIds)) {
|
|
throw new \Exception('Invalid rootId');
|
|
}
|
|
if ($relPath == '') {
|
|
$relPath = '.';
|
|
}
|
|
|
|
$absPath = PathHelper::canonicalize(Paths::getAbsDirById($rootId) . '/' . $relPath);
|
|
SanityCheck::absPathExists($absPath);
|
|
|
|
return [$absPath, $relPath, $rootId];
|
|
}
|
|
catch (\Exception $e) {
|
|
//throw new \Exception('Invalid path received (' . $e->getMessage() . ')');
|
|
throw new \Exception('Invalid path');
|
|
}
|
|
}
|
|
|
|
public static function processGetFolder() {
|
|
|
|
Validate::postHasKey('args');
|
|
|
|
//$args = json_decode(sanitize_text_field(stripslashes($_POST['args'])), true);
|
|
|
|
$args = self::getArgs();
|
|
if (!array_key_exists('path', $args)) {
|
|
throw new \Exception('"path" argument missing for command');
|
|
}
|
|
|
|
$path = SanityCheck::noStreamWrappers($args['path']);
|
|
//$pathTokens = explode('/', $path);
|
|
if ($path == '') {
|
|
$result = [
|
|
'children' => [
|
|
[
|
|
'name' => '/',
|
|
'isDir' => true,
|
|
'nickname' => 'scope'
|
|
]
|
|
]
|
|
];
|
|
return $result;
|
|
}
|
|
|
|
$config = Config::loadConfigAndFix();
|
|
$rootIds = Paths::getImageRootIds();
|
|
if ($path == '/') {
|
|
$rootIds = Paths::filterOutSubRoots($config['scope']);
|
|
$result = ['children'=>[]];
|
|
foreach ($rootIds as $rootId) {
|
|
$result['children'][] = [
|
|
'name' => $rootId,
|
|
'isDir' => true,
|
|
];
|
|
}
|
|
return $result;
|
|
}
|
|
list($absPath, $relPath, $rootId) = self::analyzePathReceived($path);
|
|
|
|
$listOptions = BulkConvert::defaultListOptions($config);
|
|
$listOptions['root'] = Paths::getAbsDirById($rootId);
|
|
|
|
$listOptions['filter']['only-unconverted'] = false;
|
|
$listOptions['flattenList'] = false;
|
|
$listOptions['max-depth'] = 0;
|
|
|
|
//throw new \Exception('Invalid rootId' . print_r($listOptions));
|
|
|
|
$list = BulkConvert::getListRecursively($relPath, $listOptions);
|
|
|
|
return ['children' => $list];
|
|
}
|
|
|
|
public static function processGetTree() {
|
|
$config = Config::loadConfigAndFix();
|
|
$rootIds = Paths::filterOutSubRoots($config['scope']);
|
|
|
|
$listOptions = [
|
|
//'root' => Paths::getUploadDirAbs(),
|
|
'ext' => $config['destination-extension'],
|
|
'destination-folder' => $config['destination-folder'], /* hm, "destination-folder" is a bad name... */
|
|
'webExpressContentDirAbs' => Paths::getWebPExpressContentDirAbs(),
|
|
'uploadDirAbs' => Paths::getUploadDirAbs(),
|
|
'useDocRootForStructuringCacheDir' => (($config['destination-structure'] == 'doc-root') && (Paths::canUseDocRootForStructuringCacheDir())),
|
|
'imageRoots' => new ImageRoots(Paths::getImageRootsDefForSelectedIds($config['scope'])), // (Paths::getImageRootsDef()
|
|
'filter' => [
|
|
'only-converted' => false,
|
|
'only-unconverted' => false,
|
|
'image-types' => $config['image-types'],
|
|
],
|
|
'flattenList' => false
|
|
];
|
|
|
|
$children = [];
|
|
foreach ($rootIds as $rootId) {
|
|
$listOptions['root'] = Paths::getAbsDirById($rootId);
|
|
$grandChildren = BulkConvert::getListRecursively('.', $listOptions);
|
|
$children[] = [
|
|
'name' => $rootId,
|
|
'isDir' => true,
|
|
'children' => $grandChildren
|
|
];
|
|
}
|
|
return ['name' => '', 'isDir' => true, 'isOpen' => true, 'children' => $children];
|
|
|
|
}
|
|
|
|
private static function getArgs() {
|
|
//return $_POST['args'];
|
|
|
|
$args = $_POST['args'];
|
|
// $args = '{\"path\":\"\"}';
|
|
//$args = '{"path":"hollo"}';
|
|
|
|
//error_log('get args:' . gettype($args));
|
|
//error_log(print_r($args, true));
|
|
//error_log(print_r(($_POST['args'] + ''), true));
|
|
|
|
//error_log('type:' . gettype($_POST['args']));
|
|
$args = json_decode('"' . $args . '"', true);
|
|
$args = json_decode($args, true);
|
|
//error_log('decoded:' . gettype($args));
|
|
//error_log(print_r($args, true));
|
|
//$args = json_decode($args, true);
|
|
|
|
return $args;
|
|
}
|
|
|
|
public static function processConvert() {
|
|
|
|
Validate::postHasKey('args');
|
|
|
|
//$args = json_decode(sanitize_text_field(stripslashes($_POST['args'])), true);
|
|
|
|
$args = self::getArgs();
|
|
if (!array_key_exists('path', $args)) {
|
|
throw new \Exception('"path" argument missing for command');
|
|
}
|
|
|
|
$path = SanityCheck::noStreamWrappers($args['path']);
|
|
|
|
$convertOptions = null;
|
|
if (isset($args['convertOptions'])) {
|
|
$convertOptions = $args['convertOptions'];
|
|
$convertOptions['log-call-arguments'] = true;
|
|
//unset($convertOptions['converter']);
|
|
//$convertOptions['png'] = ['quality' => 7];
|
|
//$convertOptions['png-quality'] = 8;
|
|
}
|
|
|
|
//error_log(print_r(json_encode($convertOptions, JSON_PRETTY_PRINT), true));
|
|
|
|
list($absPath, $relPath, $rootId) = self::analyzePathReceived($path);
|
|
|
|
$convertResult = Convert::convertFile($absPath, null, $convertOptions);
|
|
|
|
$result = [
|
|
'success' => $convertResult['success'],
|
|
'data' => $convertResult['msg'],
|
|
'log' => $convertResult['log'],
|
|
'args' => $args, // for debugging. TODO
|
|
];
|
|
$info = [];
|
|
if (isset($convertResult['filesize-webp'])) {
|
|
$info['size'] = $convertResult['filesize-webp'];
|
|
}
|
|
if (isset($convertResult['destination-url'])) {
|
|
$info['url'] = $convertResult['destination-url'] . '?' . SelfTestHelper::randomDigitsAndLetters(8);
|
|
}
|
|
if (isset($convertResult['destination-path'])) {
|
|
self::setMime($convertResult['destination-path'], $info);
|
|
}
|
|
|
|
$result['converted'] = $info;
|
|
return $result;
|
|
|
|
/*if (!array_key_exists('convertOptions', $args)) {
|
|
throw new \Exception('"convertOptions" argument missing for command');
|
|
}
|
|
//return ['success' => true, 'optionsReceived' => $args['convertOptions']];
|
|
*/
|
|
|
|
|
|
/*
|
|
$path = SanityCheck::pathWithoutDirectoryTraversal($args['path']);
|
|
$path = ltrim($path, '/');
|
|
$pathTokens = explode('/', $path);
|
|
|
|
$rootId = array_shift($pathTokens); // Shift off the first item, which is the scope
|
|
$relPath = implode('/', $pathTokens);
|
|
$config = Config::loadConfigAndFix();
|
|
$rootIds = Paths::filterOutSubRoots($config['scope']);
|
|
if (!in_array($rootId, $rootIds)) {
|
|
throw new \Exception('Invalid scope');
|
|
}
|
|
|
|
$absPath = Paths::getAbsDirById($rootId) . '/' . $relPath;
|
|
//absPathExistsAndIsFile
|
|
SanityCheck::absPathExists($absPath); */
|
|
}
|
|
|
|
public static function processDeleteConverted() {
|
|
|
|
Validate::postHasKey('args');
|
|
|
|
//$args = json_decode(sanitize_text_field(stripslashes($_POST['args'])), true);
|
|
|
|
//$args = $_POST['args'];
|
|
$args = self::getArgs();
|
|
if (!array_key_exists('path', $args)) {
|
|
throw new \Exception('"path" argument missing for command');
|
|
}
|
|
|
|
$path = SanityCheck::noStreamWrappers($args['path']);
|
|
list($absPath, $relPath, $rootId) = self::analyzePathReceived($path);
|
|
|
|
$config = Config::loadConfigAndFix();
|
|
$destinationOptions = DestinationOptions::createFromConfig($config);
|
|
if ($destinationOptions->useDocRoot) {
|
|
if (!(Paths::canUseDocRootForStructuringCacheDir())) {
|
|
$destinationOptions->useDocRoot = false;
|
|
}
|
|
}
|
|
$destinationPath = Paths::getDestinationPathCorrespondingToSource($absPath, $destinationOptions);
|
|
|
|
if (@!file_exists($destinationPath)) {
|
|
throw new \Exception('file not found: ' . $destinationPath);
|
|
}
|
|
|
|
if (@!unlink($destinationPath)) {
|
|
throw new \Exception('failed deleting file');
|
|
}
|
|
|
|
$result = [
|
|
'success' => true,
|
|
'data' => $destinationPath
|
|
];
|
|
return $result;
|
|
|
|
}
|
|
|
|
}
|