amazon-s3ckfinderckeditor5

Set common key prefix for S3 bucket per CKFinder 3 instance


How can I make the CKFinder ASP.net S3 integration load content from a dynamic key prefix rather than just a root location?


I'm using CKEditor 5 and CKFinder 3 with the ASP.net Connector to allow image upload directly to an S3 bucket. The web application we are connecting this all to is not an ASP.net application.

Setting is up was simple enough by following the documentation.

However, our product is SaaS, so each time the CKFinder is launched, I need it to target a different key prefix in our bucket. Multiple websites run off the same app and each should be able to have their own gallery of images loaded via the CKFinder without being able to see the images belonging to other apps.


Our CKFinder Web.config:

<backend name="s3Bucket" adapter="s3">
   <option name="bucket" value="myBucket" />
   <option name="key" value="KEYHERE" />
   <option name="secret" value="SECRETHERE" />
   <option name="region" value="us-east-1" />
   <option name="root" value="images" />
 </backend>

This config gets content into the /images/ common key prefix "folder" just great, but for each app that uses the CKFinder, I want it to read from a different "root":

/images/app1Id/
/images/app2Id/
/images/app3Id/

Ideally, I want to set this when invoking the Editor/Finder instance; something like:

ClassicEditor.create( document.querySelector( '#textareaId' ), {
    ckfinder: {
        uploadUrl: '/ckfinder/connector?command=QuickUpload&type=Images&responseType=json',
        connectorRoot: '/images/app1Id/'
    },
    toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'ckfinder' ],
    heading: {
        options: [
            { model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
            { model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
            { model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' }
        ]
    }
});

Here I added connectorRoot: '/images/app1Id/' as an example of what I would like to pass.

Is there some way to do something like this? I've read through the ASP.net Connector docs and see that you can build your own connector and use pass to send it data, but having to compile and maintain a custom connector does not sound very fun. The S3 connectivity here is so great and easy... if only it let me be a little more specific.


Solution

  • The solution we came to was to modify and customize the CKFinder ASP Connector. Big thanks to the CKSource team for helping us to get this running.


    ConnectorConfig.cs

    namespace CKSource.CKFinder.Connector.WebApp
    {
        using System.Configuration;
        using System.Linq;
    
        using CKSource.CKFinder.Connector.Config;
        using CKSource.CKFinder.Connector.Core.Acl;
        using CKSource.CKFinder.Connector.Core.Builders;
        using CKSource.CKFinder.Connector.Host.Owin;
        using CKSource.CKFinder.Connector.KeyValue.FileSystem;
        using CKSource.FileSystem.Amazon;
        //using CKSource.FileSystem.Azure;
        //using CKSource.FileSystem.Dropbox;
        //using CKSource.FileSystem.Ftp;
        using CKSource.FileSystem.Local;
    
        using Owin;
    
        public class ConnectorConfig
        {
            public static void RegisterFileSystems()
            {
                FileSystemFactory.RegisterFileSystem<LocalStorage>();
                //FileSystemFactory.RegisterFileSystem<DropboxStorage>();
                FileSystemFactory.RegisterFileSystem<AmazonStorage>();
                //FileSystemFactory.RegisterFileSystem<AzureStorage>();
                //FileSystemFactory.RegisterFileSystem<FtpStorage>();
            }
    
            public static void SetupConnector(IAppBuilder builder)
            {
                var allowedRoleMatcherTemplate = ConfigurationManager.AppSettings["ckfinderAllowedRole"];
                var authenticator = new RoleBasedAuthenticator(allowedRoleMatcherTemplate);
    
                var connectorFactory = new OwinConnectorFactory();
                var connectorBuilder = new ConnectorBuilder();
                var connector = connectorBuilder
                    .LoadConfig()
                    .SetAuthenticator(authenticator)
                    .SetRequestConfiguration(
                        (request, config) =>
                        {
    
                            config.LoadConfig();
    
                            var defaultBackend = config.GetBackend("default");
                            var keyValueStoreProvider = new FileSystemKeyValueStoreProvider(defaultBackend);
                            config.SetKeyValueStoreProvider(keyValueStoreProvider);
    
                            // Remove dummy resource type
                            config.RemoveResourceType("dummy");
    
                            var queryParameters = request.QueryParameters;
    
                            // This code lacks some input validation - make sure the user is allowed to access passed appId
                            string appId = queryParameters.ContainsKey("appId") ? Enumerable.FirstOrDefault(queryParameters["appId"]) : string.Empty;
    
                            // set up an array of StringMatchers for folder to hide!
                            StringMatcher[] hideFoldersMatcher = new StringMatcher[] { new StringMatcher(".*"), new StringMatcher("CVS"), new StringMatcher("thumbs"), new StringMatcher("__thumbs") };
    
                            // image type resource setup
                            var fileSystem_Images = new AmazonStorage(secret: "SECRET-HERE",
                                                                key: "KEY-HERE",
                                                                bucket: "BUCKET-HERE",
                                                                region: "us-east-1",
                                                                root: string.Format("images/{0}/userimages/", appId),
                                                                signatureVersion: "4");
    
                            string[] allowedExtentions_Images = new string[] {"gif","jpeg","jpg","png"};
    
                            config.AddBackend("s3Images", fileSystem_Images, string.Format("CDNURL-HERE/images/{0}/userimages/", appId), false);
    
                            config.AddResourceType("Images", resourceBuilder => {
                                resourceBuilder.SetBackend("s3Images", "/")
                                .SetAllowedExtensions(allowedExtentions_Images)
                                .SetHideFoldersMatchers(hideFoldersMatcher)
                                .SetMaxFileSize( 5242880 );
                            });
    
                             // file type resource setup
                            var fileSystem_Files = new AmazonStorage(secret: "SECRET-HERE",
                                                            key: "KEY-HERE",
                                                            bucket: "BUCKET-HERE",
                                                            region: "us-east-1",
                                                            root: string.Format("docs/{0}/userfiles/", appId),
                                                            signatureVersion: "4");
    
                            string[] allowedExtentions_Files = new string[] {"csv","doc","docx","gif","jpeg","jpg","ods","odt","pdf","png","ppt","pptx","rtf","txt","xls","xlsx"};
    
                            config.AddBackend("s3Files", fileSystem_Files, string.Format("CDNURL-HERE/docs/{0}/userfiles/", appId), false);
    
                            config.AddResourceType("Files", resourceBuilder => {
                                resourceBuilder.SetBackend("s3Files", "/")
                                .SetAllowedExtensions(allowedExtentions_Files)
                                .SetHideFoldersMatchers(hideFoldersMatcher)
                                .SetMaxFileSize( 10485760 );
                            });
    
                        })
                    .Build(connectorFactory);
    
                builder.UseConnector(connector);
            }
        }
    }
    

    Items of note:


    Initializing a CKEditor4/CKFinder3 instance

    <script src="/js/ckeditor/ckeditor.js"></script>
    <script src="/js/ckfinder3/ckfinder.js"></script>
    
    <script type="text/javascript">
    
        var myEditor = CKEDITOR.replace( 'bodyContent', {
    
            toolbar:                    'Default',
            width:                      '100%',
            startupMode:                'wysiwyg',
    
            filebrowserBrowseUrl:       '/js/ckfinder3/ckfinder.html?type=Files&appId=12345',
            filebrowserUploadUrl:       '/js/ckfinder3/connector?command=QuickUpload&type=Files&appId=12345',
    
            filebrowserImageBrowseUrl:  '/js/ckfinder3/ckfinder.html?type=Images&appId=12345',
            filebrowserImageUploadUrl:  '/js/ckfinder3/connector?command=QuickUpload&type=Images&appId=12345',
    
            uploadUrl:                  '/js/ckfinder3/connector?command=QuickUpload&type=Images&responseType=json&appId=12345'
    
        });
    </script>
    

    Items of note:


    ckfinder.html

    <!DOCTYPE html>
    <!--
    Copyright (c) 2007-2019, CKSource - Frederico Knabben. All rights reserved.
    For licensing, see LICENSE.html or https://ckeditor.com/sales/license/ckfinder
    -->
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
        <title>CKFinder 3 - File Browser</title>
    </head>
    <body>
    
    <script src="ckfinder.js"></script>
    <script>
    
        var urlParams = new URLSearchParams( window.location.search );
        var myAppId = ( urlParams.has( 'appId' ) ) ? urlParams.get( 'appId' ) : '';
    
        if ( myAppId !== '' ) {
            CKFinder.start( { pass: 'appId', appId: myAppId } );
        } else {
            document.write( 'Error loading configuration.' );
        }
    
    </script>
    
    </body>
    </html>
    

    Items of note: