Native Support for Assigning Mime Types For S3 On File Upload

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • blackbackpack
    Junior Member
    • Jan 2025
    • 5

    #1

    Native Support for Assigning Mime Types For S3 On File Upload

    /application/Espo/Core/FileStorage/Storages/AwsS3.php

    This file as published does not support assigning mime types when uploading

    <?php
    /************************************************** **********************
    * This file is part of EspoCRM.
    *
    * EspoCRM – Open Source CRM application.
    * Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
    * Website: https://www.espocrm.com
    *
    * This program is free software: you can redistribute it and/or modify
    * it under the terms of the GNU Affero General Public License as published by
    * the Free Software Foundation, either version 3 of the License, or
    * (at your option) any later version.
    *
    * This program is distributed in the hope that it will be useful,
    * but WITHOUT ANY WARRANTY; without even the implied warranty of
    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    * GNU Affero General Public License for more details.
    *
    * You should have received a copy of the GNU Affero General Public License
    * along with this program. If not, see <https://www.gnu.org/licenses/>.
    *
    * The interactive user interfaces in modified source and object code versions
    * of this program must display Appropriate Legal Notices, as required under
    * Section 5 of the GNU Affero General Public License version 3.
    *
    * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
    * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
    ************************************************** **********************/

    namespace Espo\Core\FileStorage\Storages;

    use Psr\Http\Message\StreamInterface;
    use AsyncAws\S3\S3Client;
    use League\Flysystem\FilesystemException;
    use League\Flysystem\AsyncAwsS3\AsyncAwsS3Adapter;
    use League\Flysystem\Filesystem;
    use GuzzleHttp\Psr7\Stream;

    use Espo\Core\FileStorage\Attachment;
    use Espo\Core\FileStorage\Storage;
    use Espo\Core\Utils\Config;

    use RuntimeException;

    /**
    * @noinspection PhpUnused
    */
    class AwsS3 implements Storage
    {
    private Filesystem $filesystem;

    public function __construct(Config $config)
    {
    $bucketName = $config->get('awsS3Storage.bucketName') ?? null;
    $path = $config->get('awsS3Storage.path') ?? null;
    $region = $config->get('awsS3Storage.region') ?? null;
    $credentials = $config->get('awsS3Storage.credentials') ?? null;
    $endpoint = $config->get('awsS3Storage.endpoint') ?? null;
    $pathStyleEndpoint = $config->get('awsS3Storage.pathStyleEndpoint') ?? false;
    $sendChunkedBody = $config->get('awsS3Storage.sendChunkedBody') ?? null;

    if (!$bucketName) {
    throw new RuntimeException("AWS S3 bucket name is not specified in config.");
    }

    $clientOptions = [
    'region' => $region,
    ];

    if ($endpoint) {
    $clientOptions['endpoint'] = $endpoint;
    }

    if ($pathStyleEndpoint) {
    $clientOptions['pathStyleEndpoint'] = (bool) $pathStyleEndpoint;
    }

    // Defaulted to true in the library, but the docs is not clear enough.
    if ($sendChunkedBody !== null) {
    $clientOptions['sendChunkedBody'] = (bool) $sendChunkedBody;
    }

    if ($credentials && is_array($credentials)) {
    $clientOptions['accessKeyId'] = $credentials['key'] ?? null;
    $clientOptions['accessKeySecret'] = $credentials['secret'] ?? null;
    }

    $client = new S3Client($clientOptions);
    $adapter = new AsyncAwsS3Adapter($client, $bucketName, $path);

    $this->filesystem = new Filesystem($adapter);
    }

    public function unlink(Attachment $attachment): void
    {
    try {
    $this->filesystem->delete($attachment->getSourceId());
    } catch (FilesystemException $e) {
    throw new RuntimeException($e->getMessage(), 0, $e);
    }
    }

    public function exists(Attachment $attachment): bool
    {
    try {
    return $this->filesystem->fileExists($attachment->getSourceId());
    } catch (FilesystemException $e) {
    throw new RuntimeException($e->getMessage(), 0, $e);
    }
    }

    public function getSize(Attachment $attachment): int
    {
    try {
    return $this->filesystem->fileSize($attachment->getSourceId());
    } catch (FilesystemException $e) {
    throw new RuntimeException($e->getMessage(), 0, $e);
    }
    }

    public function getStream(Attachment $attachment): StreamInterface
    {
    try {
    $resource = $this->filesystem->readStream($attachment->getSourceId());
    } catch (FilesystemException $e) {
    throw new RuntimeException($e->getMessage(), 0, $e);
    }

    return new Stream($resource);
    }

    public function putStream(Attachment $attachment, StreamInterface $stream): void
    {
    // League\Flysystem does not support StreamInterface.
    // Need to pass a resource.

    $resource = fopen('php://temp', 'r+');

    if ($resource === false) {
    throw new RuntimeException("Could not open temp.");
    }

    $stream->rewind();

    fwrite($resource, $stream->getContents());
    rewind($resource);

    try {
    $this->filesystem->writeStream($attachment->getSourceId(), $resource);
    } catch (FilesystemException $e) {
    throw new RuntimeException($e->getMessage(), 0, $e);
    }

    fclose($resource);
    }
    }

    This modification resolves that:

    public function putStream(Attachment $attachment, StreamInterface $stream): void
    {
    $mimeType = null;

    if ($attachment instanceof \Espo\Core\FileStorage\AttachmentEntityWrapper) {
    $reflection = new \ReflectionClass($attachment);
    $property = $reflection->getProperty('attachment');
    $property->setAccessible(true);

    /** @var \Espo\Entities\Attachment $attachmentEntity */
    $attachmentEntity = $property->getValue($attachment);

    if (method_exists($attachmentEntity, 'getType')) {
    $mimeType = $attachmentEntity->getType();
    }
    }

    if (!$mimeType) {
    throw new RuntimeException("MIME type is missing or inaccessible for attachment.");
    }

    $resource = fopen('php://temp', 'r+');

    if ($resource === false) {
    throw new RuntimeException("Could not open temp.");
    }

    $stream->rewind();

    fwrite($resource, $stream->getContents());
    rewind($resource);

    $metadata = [
    'ContentType' => $mimeType,
    ];

    try {
    $this->filesystem->writeStream($attachment->getSourceId(), $resource, $metadata);
    } catch (FilesystemException $e) {
    throw new RuntimeException($e->getMessage(), 0, $e);
    }

    fclose($resource);
    }
    }

    I am sure the espo development team can come up with a cleaner solution to handle this and am hoping that it can be incorporated into the next upgrade so I do not have to worry about manually updating this file every time I upgrade.​
  • blackbackpack
    Junior Member
    • Jan 2025
    • 5

    #2
    I have some feedback on this file /application/Espo/Core/FileStorage/Storages/AwsS3.php
    The built in processes when uploading files to S3 do not contain support for assigning Mime Types
    I was able to modify the file to fix the issue, but when I upgraded recently I had to make the modifications again because it got overwritten
    I am hoping there is a way for me to share this feedback in the correct place so hopefully future upgrades will support assigning Mime types natively

    It will not let me post because I am a new user

    How can I get this feedback to the right place/person?

    Comment

    • yuri
      Member
      • Mar 2014
      • 9145

      #3
      Could you explain why it's important to have the content type for you? Do you access the files directly on S3? As they are named as IDs without directories, I wouldn't expect that the content-type could be needed.

      You can also consider to create a custom class without overriding the core file. In this case it will be upgrade-safe.
      Last edited by yuri; 01-23-2025, 08:40 PM.
      If you find EspoCRM good, we would greatly appreciate if you could give the project a star on GitHub. We believe our work truly deserves more recognition. Thanks.

      Comment

      • blackbackpack
        Junior Member
        • Jan 2025
        • 5

        #4
        Thank you for your response

        Reasons why I want to include it:

        Proper File Handling by Browsers

        Security (Incorrect MIME types can create security vulnerabilities, such as **MIME type sniffing attacks)

        Application Compatibility

        Improved User Experience

        Cross-Origin Resource Sharing (CORS)

        Compliance with Standards

        Custom file delivery functions

        I will look into creating a custom class to handle this so it won’t be overwritten

        is there any advantage to NOT assigning the mime type?

        Comment

        • yuri
          Member
          • Mar 2014
          • 9145

          #5
          > is there any advantage to NOT assigning the mime type

          We would need to test it. There can be different providers. There's a chance that something goes wrong on some provider we even not aware of.

          But I'm still not convinced that there's any benefit of sending mime-types. Espo stores mime-types internally.
          If you find EspoCRM good, we would greatly appreciate if you could give the project a star on GitHub. We believe our work truly deserves more recognition. Thanks.

          Comment

          • blackbackpack
            Junior Member
            • Jan 2025
            • 5

            #6
            Well thank you for your consideration and for your suggestion in creating a custom class so I do not have to manually update when I upgrade versions

            Comment

            • blackbackpack
              Junior Member
              • Jan 2025
              • 5

              #7
              So far this platform has been really awesome to work with and I appreciate all the hard work you all put in. I gave it a star on github.

              Comment

              • chris8411
                Junior Member
                • Feb 2025
                • 1

                #8
                Originally posted by yuri
                > is there any advantage to NOT assigning the mime type

                We would need to test it. There can be different providers. There's a chance that something goes wrong on some provider we even not aware of.

                But I'm still not convinced that there's any benefit of sending mime-types. Espo stores mime-types internally.
                I'm not sure which direction is meant by "sending mime-types", but we have a similar problem and regarding the "benefit": The benefit of sending the mime-type during download to the browser would be to have a working download behaviour in Firefox for basically anything that is not "text/plain" and during upload to make the correct behaviour later possible, in the first place.

                Our Sales team recently transitioned from some CRM to EspoCRM, where I coded the transformer and - as much as they love it - they cannot open Outlook E-Mails they attached. The original data set contained attachments, that I uploaded via the API with the correct mime-type set and those files work well, but anything they are uploading now, is not usable for them without cumbersome technical trickery, which is also very confusing for them, no matter how easy this is for people with technical background.

                Changing PHP code, adding classes etc. is just a 100% no-go for us, because it makes updating too complicated and error-prone. As long as this is not fixed, we will stick with adding a service as a separate Docker container, that regurarly updates the mime-types in the database. This is also very dangerous, so we are very much looking forward to hearing about a fix.

                Anyway, thanks so far for your work.

                Comment

                Working...