codebar-ag / laravel-flysystem-cloudinary

Cloudinary Flysystem integration with Laravel.
https://www.cloudinary.com
MIT License
8 stars 5 forks source link

[Question]: Although Images are uploaded, the generated urls get 404 #56

Closed azim-kordpour closed 2 months ago

azim-kordpour commented 6 months ago

Question or Feature?

Hi, To make a long story short, the images are uploaded and I can see them in the cloudinary panel but the generated URLs get 404.

I'm using filament and Spatie Media Library and these are my configurations:

in filesystems.php:

        'cloudinary' => [
            'driver' => 'cloudinary',
            'cloud_name' => env('CLOUDINARY_CLOUD_NAME'),
            'api_key' => env('CLOUDINARY_API_KEY'),
            'api_secret' => env('CLOUDINARY_API_SECRET'),
            'url' => [
                'secure' => (bool) env('CLOUDINARY_SECURE_URL', true),
            ],

in .env

FILESYSTEM_DISK=cloudinary
FILAMENT_FILESYSTEM_DISK=cloudinary

FILESYSTEM_DRIVER=cloudinary
CLOUDINARY_CLOUD_NAME=cloud-name
CLOUDINARY_API_KEY=111111111
CLOUDINARY_API_SECRET=1111111
CLOUDINARY_FOLDER=test

The URL is like this:

https://res.cloudinary.com/cloud-name/image/upload/v1/test/5/01HSTPYVKKM2GP417CHR1NG2R7.jpg?_a=BAACwmE0

Could you please guide me?

RhysLees commented 6 months ago

@azim-kordpour it seems you are missing the folder setting in filesystems.php

        'cloudinary' => [
            'driver' => 'cloudinary',
            'cloud_name' => env('CLOUDINARY_CLOUD_NAME'),
            'api_key' => env('CLOUDINARY_API_KEY'),
            'api_secret' => env('CLOUDINARY_API_SECRET'),
            'url' => [
                'secure' => (bool) env('CLOUDINARY_SECURE_URL', true),
            ],
            'folder' => env('CLOUDINARY_FOLDER'),
        ],

Could you update this?

Please share what is the expected url should be.

Please share how you have implemented with Filament.

azim-kordpour commented 6 months ago

The problem is extension. When I set LIVEWIRE_TEMPORARY_DISK=cloudinary I get this message

Illuminate\Filesystem\FilesystemAdapter::Illuminate\Filesystem{closure}(): Argument #1 ($attributes) must be of type League\Flysystem\StorageAttributes, array given, called in /var/www/html/vendor/league/flysystem/src/DirectoryListing.php on line 33

I checked the temporary directory of Livewire on the Cloudinary panel and the picture was there.

Then, I changed LIVEWIRE_TEMPORARY_DISK=public, and the upload was done, but the image did not displayed (404).

This was the URL:

https://res.cloudinary.com/cloud-name/image/upload/v1/testing-folderl/5/01HT2BYSR6JWREQQY46DF3QN4E.jpg?_a=BA

and this works:

https://res.cloudinary.com/cloud-name/image/upload/v1/testing-folderl/5/01HT2BYSR6JWREQQY46DF3QN4E.jpg.jpg?_a=BA

It is mentioned in the document not to save the extension. It's not an appropriate approach to save a file without an extension and retrieve it with an extension.

StanBarrows commented 6 months ago

Hey @azim-kordpour

Thanks for your message.

We save and submit Cloudinary's Public ID to the database. So whatever Key you send to Cloudinary is the PublicID, not the FilePath. The extension needs to be specified when you retrieve the file. Thats why your second example works.

01HT2BYSR6JWREQQY46DF3QN4E.jpg => Cloudinary Public ID

01HT2BYSR6JWREQQY46DF3QN4E.jpg.jpg => Get Image as JPG
01HT2BYSR6JWREQQY46DF3QN4E.jpg.png => Get Image as PNG
01HT2BYSR6JWREQQY46DF3QN4E.jpg.webp => Get Image as WEBP

CleanShot 2024-03-28 at 13 05 58@2x

We use a helper method on the model to retrieve Cloudinary images and specify the format there.

   public function getImage($imageFormat = '.webp')
    {
       switch ($this->image_disk) {
            case 'url':
            case 'cloudinary':
                return $this->image . $imageFormat;
            default:
                return Storage::disk($this->image_disk)->url($this->image);
        }
    }

Best regards Sebastian

StanBarrows commented 6 months ago

Hey @azim-kordpour

I've updated my previous post.

Best regards Sebastian

azim-kordpour commented 6 months ago

@StanBarrows

Thank you for the explanation.

To solve my problem, I changed the media model in the configuration file of the media library to override file_name by an accessor.

StanBarrows commented 6 months ago

Hey @azim-kordpour

We can not return a working string or reference to the file because we're currently unaware of the file type (extension). Although we could store it directly within the Cloudinary Public ID, I remember that using dots "." in the ID caused problems in the past, so we decided against it.

I am open to creating a concept to save the Cloudinary input in a JSON Database field.

[
'file_disk'=> cloudinary, 
'file_type'=> 'jpg' 
''file_id' => '12345'
]

However, this would make the whole integration more complex, and another downside would be that it would then be difficult to switch between Laravel storage drivers just by changing the filesystem's .env values.

In our current applications, We use for every field that is linked to a storage driver a "field_disk" column so that I can always use multiple drivers in my application. For example, for testing.

But, we will update and improve the docs so that it is clear that the handling of this package is more related to storing and updating data in Cloudinary. The handling needs to be done based on the integration usage. We'll shortly provide a Fillament Image Field.

robclancy commented 5 months ago

For filament I have taken their random filenames from here https://github.com/filamentphp/filament/blob/3.x/packages/forms/src/Components/BaseFileUpload.php#L180.

And applied it up cloudinary uploads without the extension.

FileUpload::make('fallback_images')
    ->image()
    ->multiple()
    ->getUploadedFileNameForStorageUsing(
        fn (): string => (string) Str::ulid()
    )
    ->disk('cloudinary'),
]),

If you need the original extension you can just add it as another attribute to save. (https://filamentphp.com/docs/3.x/forms/fields/file-upload#storing-original-file-names-independently)

I am probably going to disable the api calls on load too since it delays a bit. https://filamentphp.com/docs/3.x/forms/fields/file-upload#prevent-file-information-fetching

And here is my full config which applies only when you use ->image() in the FileUpload.

FileUpload::configureUsing(function (FileUpload $fileUpload) {
    $fileUpload->disk(function (FileUpload $component) {
        // this means the FileUpload was created with `->image()`
        if ($component->getAcceptedFileTypes() == ['image/*']) {
            return 'cloudinary';
        }

        return null;
    })->fetchFileInformation(function (FileUpload $component) {
        return ! ($component->getDisk()->getAdapter() instanceof FlysystemCloudinaryAdapter);
    });
});

FileUpload::configureUsing(function (FileUpload $fileUpload) {
    $fileUpload->getUploadedFileNameForStorageUsing(function (FileUpload $component, TemporaryUploadedFile $file) {
        if ($component->getDisk()->getAdapter() instanceof FlysystemCloudinaryAdapter) {
            return Str::ulid();
        }

        return $component->shouldPreserveFilenames() ? $file->getClientOriginalName() : (Str::ulid().'.'.$file->getClientOriginalExtension());
    });
}, isImportant: true);
henryonsoftware commented 4 months ago

@robclancy Do you know how to change the file when using TextEditor file attachment. Your random file name in getUploadedFileNameForStorageUsing is great but Idk how to fix it also in the editor file attachments.

Problem: the uploaded attachment file will also use the extension(.jpg, .pne) so it can't be render after uploaded.

henryonsoftware commented 4 months ago

Oh I found out

TinyEditor::make('body')
  ->toolbarSticky(true)
  ->required()
  ->columnSpanFull()
  ->fileAttachmentsDisk('cloudinary')
  ->fileAttachmentsDirectory('blogs')
  ->saveUploadedFileAttachmentsUsing(function (TemporaryUploadedFile $file) {
    return $file->storeAs('blogs', Str::random(30), 'cloudinary');
  }),
robclancy commented 4 months ago

Yep. I have done similar. This is my final workaround.

if (config('filament.default_filesystem_disk') == 'cloudinary') {
    FileUpload::configureUsing(function (FileUpload $fileUpload) {
        $fileUpload->fetchFileInformation(false);
    });

    FileUpload::configureUsing(function (FileUpload $fileUpload) {
        $fileUpload->getUploadedFileNameForStorageUsing(function () {
            return Str::ulid();
        });
    }, isImportant: true);

    RichEditor::configureUsing(function (RichEditor $editor) {
        $editor->saveUploadedFileAttachmentsUsing(function (RichEditor $editor, TemporaryUploadedFile $file) {
            return $file->storePubliclyAs($editor->getFileAttachmentsDirectory(), Str::ulid(), $editor->getFileAttachmentsDiskName());
        });
    });
}