Make storage system compatible with remote storages - closes #5603
This commit is contained in:
@@ -19,8 +19,6 @@ class Attachment extends Model
|
||||
|
||||
const DIRECTORY = 'attachment';
|
||||
|
||||
const DISK = 'private';
|
||||
|
||||
const MIME_TYPE_MAX_LENGTH = 127;
|
||||
|
||||
// This token type was used for backward compatibility for some time
|
||||
@@ -135,7 +133,7 @@ class Attachment extends Model
|
||||
$file_info = self::saveFileToDisk($attachment, $file_name, $content, $uploaded_file);
|
||||
|
||||
$attachment->file_dir = $file_info['file_dir'];
|
||||
$attachment->size = Storage::disk(self::DISK)->size($file_info['file_path']);
|
||||
$attachment->size = Storage::size($file_info['file_path']);
|
||||
$attachment->save();
|
||||
|
||||
return $attachment;
|
||||
@@ -143,6 +141,7 @@ class Attachment extends Model
|
||||
|
||||
/**
|
||||
* Save file to the disk and return file_dir.
|
||||
* $content may be a resource pointing to the file in remote storage.
|
||||
*/
|
||||
public static function saveFileToDisk($attachment, $file_name, $content, $uploaded_file)
|
||||
{
|
||||
@@ -155,21 +154,24 @@ class Attachment extends Model
|
||||
do {
|
||||
$i++;
|
||||
$file_path = self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir.$i.DIRECTORY_SEPARATOR.$file_name;
|
||||
} while (Storage::disk(self::DISK)->exists($file_path));
|
||||
} while (Storage::exists($file_path));
|
||||
|
||||
$file_dir .= $i.DIRECTORY_SEPARATOR;
|
||||
|
||||
try {
|
||||
if ($uploaded_file) {
|
||||
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, ['disk' => self::DISK]);
|
||||
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name);
|
||||
} else {
|
||||
Storage::disk(self::DISK)->put($file_path, $content);
|
||||
Storage::put($file_path, $content);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Helper::logException($e, '[Attachment::saveFileToDisk()]');
|
||||
}
|
||||
|
||||
\Helper::sanitizeUploadedFileData($file_path, \Helper::getPrivateStorage(), $content);
|
||||
// $content may be a stream resource rather than a string; the SVG sanitizer needs
|
||||
// actual string content, so let it re-read the just-written file in that case instead
|
||||
// of treating the resource as file content.
|
||||
\Helper::sanitizeUploadedFileData($file_path, is_resource($content) ? null : $content);
|
||||
|
||||
return [
|
||||
'file_dir' => $file_dir,
|
||||
@@ -266,7 +268,7 @@ class Attachment extends Model
|
||||
// URL must contain only forward slashes.
|
||||
$file_path = str_replace(DIRECTORY_SEPARATOR, '/', $file_path);
|
||||
|
||||
$file_url = Storage::url($file_path);
|
||||
$file_url = Storage::disk('local')->url($file_path);
|
||||
|
||||
// Fix percents.
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3530
|
||||
@@ -306,12 +308,10 @@ class Attachment extends Model
|
||||
$file_name = $file_name.'.eml';
|
||||
}
|
||||
|
||||
return $this->getDisk()->download($this->getStorageFilePath(), $file_name, $headers);
|
||||
}
|
||||
// Cache attachments in browser - 1 month.
|
||||
$headers['Cache-Control'] = 'max-age=2592000';
|
||||
|
||||
private function getDisk()
|
||||
{
|
||||
return Storage::disk(self::DISK);
|
||||
return Storage::download($this->getStorageFilePath(), $file_name, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -338,7 +338,7 @@ class Attachment extends Model
|
||||
public function getLocalFilePath($full = true)
|
||||
{
|
||||
if ($full) {
|
||||
return $this->getDisk()->path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||
return Storage::path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||
} else {
|
||||
return DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name;
|
||||
}
|
||||
@@ -349,7 +349,7 @@ class Attachment extends Model
|
||||
*/
|
||||
public function fileExists()
|
||||
{
|
||||
return $this->getDisk()->exists(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||
return Storage::exists(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||
}
|
||||
|
||||
public static function formatBytes($size, $precision = 0)
|
||||
@@ -400,7 +400,7 @@ class Attachment extends Model
|
||||
{
|
||||
// Delete from disk
|
||||
foreach ($attachments as $attachment) {
|
||||
$attachment->getDisk()->delete($attachment->getStorageFilePath());
|
||||
Storage::delete($attachment->getStorageFilePath());
|
||||
}
|
||||
|
||||
// Delete from DB
|
||||
@@ -448,12 +448,20 @@ class Attachment extends Model
|
||||
$new_attachment->save();
|
||||
|
||||
try {
|
||||
$attachment_file = new \Illuminate\Http\UploadedFile(
|
||||
$this->getLocalFilePath(), $this->file_name,
|
||||
null, null, true
|
||||
);
|
||||
$content_stream = '';
|
||||
$attachment_file = null;
|
||||
|
||||
$file_info = Attachment::saveFileToDisk($new_attachment, $new_attachment->file_name, '', $attachment_file);
|
||||
if (\Helper::isLocalStorage()) {
|
||||
$attachment_file = new \Illuminate\Http\UploadedFile(
|
||||
$this->getLocalFilePath(), $this->file_name,
|
||||
null, null, true
|
||||
);
|
||||
} else {
|
||||
// File in remote storage.
|
||||
$content_stream = $this->getFileStream();
|
||||
}
|
||||
|
||||
$file_info = Attachment::saveFileToDisk($new_attachment, $new_attachment->file_name, $content_stream, $attachment_file);
|
||||
|
||||
if (!empty($file_info['file_dir'])) {
|
||||
$new_attachment->file_dir = $file_info['file_dir'];
|
||||
@@ -468,6 +476,16 @@ class Attachment extends Model
|
||||
|
||||
public function getFileContents()
|
||||
{
|
||||
return $this->getDisk()->get($this->getStorageFilePath());
|
||||
return Storage::get($this->getStorageFilePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as getFileContents(), but as a stream instead of loading the whole
|
||||
* file into memory - use this when the bytes are only being copied
|
||||
* elsewhere (e.g. duplicating/forwarding an attachment).
|
||||
*/
|
||||
public function getFileStream()
|
||||
{
|
||||
return Storage::readStream($this->getStorageFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2301,12 +2301,20 @@ class Conversation extends Model
|
||||
$new_attachment->save();
|
||||
|
||||
try {
|
||||
$attachment_file = new \Illuminate\Http\UploadedFile(
|
||||
$attachment->getLocalFilePath(), $attachment->file_name,
|
||||
null, null, true
|
||||
);
|
||||
$content_stream = '';
|
||||
$attachment_file = null;
|
||||
|
||||
$file_info = Attachment::saveFileToDisk($new_attachment, $new_attachment->file_name, '', $attachment_file);
|
||||
if (\Helper::isLocalStorage()) {
|
||||
$attachment_file = new \Illuminate\Http\UploadedFile(
|
||||
$attachment->getLocalFilePath(), $attachment->file_name,
|
||||
null, null, true
|
||||
);
|
||||
} else {
|
||||
// File in remote storage.
|
||||
$content_stream = $this->getFileStream();
|
||||
}
|
||||
|
||||
$file_info = Attachment::saveFileToDisk($new_attachment, $new_attachment->file_name, $content_stream, $attachment_file);
|
||||
|
||||
if (!empty($file_info['file_dir'])) {
|
||||
$new_attachment->file_dir = $file_info['file_dir'];
|
||||
|
||||
@@ -1404,7 +1404,8 @@ class Customer extends Model
|
||||
}
|
||||
|
||||
$file_name = md5(Hash::make($this->id)).'.jpg';
|
||||
$dest_path = Storage::path(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name);
|
||||
$storage_path = self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name;
|
||||
$dest_path = Storage::disk('local')->path($storage_path);
|
||||
|
||||
$dest_dir = pathinfo($dest_path, PATHINFO_DIRNAME);
|
||||
if (!file_exists($dest_dir)) {
|
||||
@@ -1418,6 +1419,8 @@ class Customer extends Model
|
||||
|
||||
imagejpeg($resized_image, $dest_path, self::PHOTO_QUALITY);
|
||||
|
||||
$file_name = \Eventy::filter('customer.save_photo', $file_name, $this, $dest_path, $storage_path);
|
||||
|
||||
return $file_name;
|
||||
}
|
||||
|
||||
|
||||
@@ -1813,12 +1813,14 @@ class ConversationsController extends Controller
|
||||
$attachment_copy = $attachment;
|
||||
}
|
||||
|
||||
$attachments[] = [
|
||||
'id' => encrypt($attachment_copy->id),
|
||||
'name' => $attachment_copy->file_name,
|
||||
'size' => $attachment_copy->size,
|
||||
'url' => $attachment_copy->url(),
|
||||
];
|
||||
if ($attachment_copy) {
|
||||
$attachments[] = [
|
||||
'id' => encrypt($attachment_copy->id),
|
||||
'name' => $attachment_copy->file_name,
|
||||
'size' => $attachment_copy->size,
|
||||
'url' => $attachment_copy->url(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ class OpenController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// CSP header for exatra security.
|
||||
// CSP header for extra security.
|
||||
$csp_header_value = "script-src 'none'; frame-src 'none'; object-src 'none'; font-src 'none'; connect-src 'none'; media-src 'self'; form-action 'none'; base-uri 'none'; sandbox";
|
||||
|
||||
// https://github.com/freescout-help-desk/freescout/issues/5281
|
||||
@@ -275,6 +275,7 @@ class OpenController extends Controller
|
||||
$csp_header_value .= ' allow-same-origin';
|
||||
}
|
||||
|
||||
// Send file.
|
||||
if (config('app.download_attachments_via') == 'apache') {
|
||||
// Send using Apache mod_xsendfile.
|
||||
$response = response(null)
|
||||
@@ -303,8 +304,8 @@ class OpenController extends Controller
|
||||
}
|
||||
} else {
|
||||
// Send via PHP.
|
||||
// In this case downloading or showing the attachment
|
||||
// is controlled via web server config or /storage/app/public/.htaccess.
|
||||
// In this case whether the file is viewed or downloaded is controlled
|
||||
// via the web server config or /storage/app/public/.htaccess.
|
||||
$headers = [
|
||||
'Content-Type' => $attachment->mime_type,
|
||||
// Laravel adds 'nosniff' by itself.
|
||||
@@ -318,7 +319,7 @@ class OpenController extends Controller
|
||||
// Browsers disable seeking without it, so audio and video attachments
|
||||
// can not be rewound or fast forwarded. BinaryFileResponse handles
|
||||
// Range requests, so use it whenever the file is shown in the browser.
|
||||
if ($view_attachment && $attachment->fileExists()) {
|
||||
if ($view_attachment && \Helper::isLocalStorage() && $attachment->fileExists()) {
|
||||
$response = response()->file(
|
||||
$attachment->getLocalFilePath(),
|
||||
$headers
|
||||
|
||||
@@ -160,10 +160,14 @@ class ReplyToCustomer extends Mailable
|
||||
|
||||
if ($thread->has_attachments) {
|
||||
foreach ($thread->attachments as $attachment) {
|
||||
if ($attachment->fileExists()) {
|
||||
$message->attach($attachment->getLocalFilePath());
|
||||
if (\Helper::isLocalStorage()) {
|
||||
if ($attachment->fileExists()) {
|
||||
$message->attach($attachment->getLocalFilePath());
|
||||
} else {
|
||||
\Log::error('[ReplyToCustomer] Thread: '.$thread->id.'. Attachment file not find on disk: '.$attachment->getLocalFilePath());
|
||||
}
|
||||
} else {
|
||||
\Log::error('[ReplyToCustomer] Thread: '.$thread->id.'. Attachment file not find on disk: '.$attachment->getLocalFilePath());
|
||||
\Eventy::filter('email.reply_to_customer.attach', false, $message, $attachment, $thread->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,12 +949,12 @@ class Helper
|
||||
|
||||
public static function getPrivateStorage()
|
||||
{
|
||||
return \Storage::disk('private');
|
||||
return \Storage::disk('local');
|
||||
}
|
||||
|
||||
public static function getPublicStorage()
|
||||
{
|
||||
return \Storage::disk('public');
|
||||
return \Storage::disk('local');
|
||||
}
|
||||
|
||||
public static function formatException($e)
|
||||
@@ -1843,22 +1843,22 @@ class Helper
|
||||
|
||||
$file->storeAs('uploads', $file_name);
|
||||
|
||||
self::sanitizeUploadedFileData('uploads'.DIRECTORY_SEPARATOR.$file_name, self::getPublicStorage());
|
||||
self::sanitizeUploadedFileData('uploads'.DIRECTORY_SEPARATOR.$file_name);
|
||||
|
||||
return self::uploadedFilePath($file_name);
|
||||
}
|
||||
|
||||
public static function sanitizeUploadedFileData($file_path, $storage, $content = null)
|
||||
public static function sanitizeUploadedFileData($file_path, $content = null)
|
||||
{
|
||||
// Remove <script>, href="", iframe, etc from SVG files.
|
||||
// Any image can be interpreted as SVG by browser,
|
||||
// so checking extension is not enough.
|
||||
if ($storage->exists($file_path)
|
||||
&& ($storage->mimeType($file_path) == 'image/svg+xml'
|
||||
if (Storage::exists($file_path)
|
||||
&& (Storage::mimeType($file_path) == 'image/svg+xml'
|
||||
|| strtolower(pathinfo($file_path, PATHINFO_EXTENSION)) == 'svg')
|
||||
) {
|
||||
if (!$content) {
|
||||
$content = $storage->get($file_path);
|
||||
$content = Storage::get($file_path);
|
||||
}
|
||||
if ($content) {
|
||||
// Remove comments from SVG content.
|
||||
@@ -1871,7 +1871,7 @@ class Helper
|
||||
if (!$clean_content) {
|
||||
$clean_content = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $content);
|
||||
}
|
||||
$storage->put($file_path, $clean_content);
|
||||
Storage::put($file_path, $clean_content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3265,4 +3265,13 @@ class Helper
|
||||
}
|
||||
array_set(self::$memory_cache, $key, $value);
|
||||
}
|
||||
|
||||
public static function isLocalStorage($disk = '')
|
||||
{
|
||||
if ($disk) {
|
||||
return \Storage::disk($disk)->getDriver()->getAdapter() instanceof \League\Flysystem\Adapter\Local;
|
||||
} else {
|
||||
return config('filesystems.default') == 'local';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -929,7 +929,8 @@ class User extends Authenticatable
|
||||
}
|
||||
|
||||
$file_name = md5(Hash::make($this->id)).'.jpg';
|
||||
$dest_path = Storage::path(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name);
|
||||
$storage_path = self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name;
|
||||
$dest_path = Storage::disk('local')->path($storage_path);
|
||||
|
||||
$dest_dir = pathinfo($dest_path, PATHINFO_DIRNAME);
|
||||
if (!file_exists($dest_dir)) {
|
||||
@@ -946,6 +947,8 @@ class User extends Authenticatable
|
||||
// User::PHOTO_DIRECTORY, !Hash::make($user->id).'.jpg'
|
||||
// );
|
||||
|
||||
$file_name = \Eventy::filter('user.save_photo', $file_name, $this, $dest_path, $storage_path);
|
||||
|
||||
return $file_name;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ return [
|
||||
'visibility' => 'public',
|
||||
],
|
||||
|
||||
'public' => [
|
||||
/*'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
@@ -63,7 +63,7 @@ return [
|
||||
'root' => storage_path('app'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'visibility' => 'public',
|
||||
],
|
||||
],*/
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
@@ -71,6 +71,10 @@ return [
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
// URL is used to form file URLs.
|
||||
'url' => env('AWS_URL'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# Force /storage/* files to be downloaded instead of showing them in the browser.
|
||||
|
||||
# The list should be in sync with /config/app.php and nginx config.
|
||||
<FilesMatch "\.((?!(jpg|jpeg|jfif|pjpeg|pjp|apng|bmp|gif|ico|cur|png|tif|tiff|webp|pdf|txt|diff|patch|json|mp3|wav|ogg|wma)).)*$">
|
||||
ForceType application/octet-stream
|
||||
|
||||
@@ -175,6 +175,7 @@ sudo echo 'server {
|
||||
# For non-static content application adds nosniff by itself
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
}
|
||||
# Force /storage/* files to be downloaded instead of showing them in the browser.
|
||||
# The list should be in sync with /storage/app/public/uploads/.htaccess and /config/app.php
|
||||
location ~* ^/storage/.*\.((?!(jpg|jpeg|jfif|pjpeg|pjp|apng|bmp|gif|ico|cur|png|tif|tiff|webp|pdf|txt|diff|patch|json|mp3|wav|ogg|wma)).)*$ {
|
||||
add_header Content-disposition "attachment; filename=$2";
|
||||
|
||||
Reference in New Issue
Block a user