├── .gitignore ├── .ruby-version ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── mountable_file_server.rb └── mountable_file_server │ ├── adapter.rb │ ├── file_accessor.rb │ ├── metadata.rb │ ├── rails.rb │ ├── server.rb │ ├── storage.rb │ ├── unique_identifier.rb │ ├── uri.rb │ └── version.rb ├── mountable_file_server.gemspec ├── shell.nix ├── test ├── acceptance │ └── form_interactions_test.rb ├── acceptance_helper.rb ├── fixtures │ ├── david.jpg │ ├── image.png │ └── test.txt ├── integration │ └── server_test.rb ├── integration_helper.rb ├── rails-dummy │ ├── README.rdoc │ ├── Rakefile │ ├── app │ │ ├── assets │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── javascripts │ │ │ │ ├── application.js │ │ │ │ └── users.js │ │ │ └── stylesheets │ │ │ │ ├── application.css │ │ │ │ ├── scaffold.css │ │ │ │ └── users.css │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ └── users_controller.rb │ │ ├── helpers │ │ │ ├── application_helper.rb │ │ │ └── users_helper.rb │ │ ├── mailers │ │ │ └── .keep │ │ ├── models │ │ │ ├── .keep │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ └── user.rb │ │ └── views │ │ │ ├── layouts │ │ │ └── application.html.erb │ │ │ └── users │ │ │ ├── _form.html.erb │ │ │ ├── edit.html.erb │ │ │ ├── index.html.erb │ │ │ ├── new.html.erb │ │ │ └── show.html.erb │ ├── bin │ │ ├── bundle │ │ ├── rails │ │ └── rake │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── assets.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── cookies_serializer.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── inflections.rb │ │ │ ├── mime_types.rb │ │ │ ├── mountable_file_server.rb │ │ │ ├── session_store.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── routes.rb │ │ └── secrets.yml │ ├── db │ │ ├── migrate │ │ │ └── 20150305204311_create_users.rb │ │ └── schema.rb │ ├── lib │ │ └── assets │ │ │ └── .keep │ ├── log │ │ └── .keep │ └── public │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ └── favicon.ico ├── support │ └── path_helper.rb ├── unit │ ├── adapter_test.rb │ ├── file_accessor_test.rb │ ├── metadata_test.rb │ ├── storage_test.rb │ ├── unique_identifier_test.rb │ └── uri_test.rb └── unit_helper.rb └── vendor └── assets └── javascripts └── mountable_file_server.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | *.log 16 | *.sqlite3 17 | test/rails-dummy/tmp/ 18 | test/rails-dummy/uploads/ 19 | /.local-data/ 20 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.8 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.3 4 | - 2.1.7 5 | - 2.0.0 6 | - 1.9.3 7 | before_script: 8 | - cd test/rails-dummy 9 | - bundle exec rake db:create db:migrate 10 | - cd ../../ 11 | script: 12 | - bundle exec rake test 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.0.3 2 | * Relax required version number of `dry-configurable` dependency 3 | 4 | # 3.0.2 5 | * Fix deprecation warning when calling metadata class initializer with keyword arguments. 6 | * Don't break when uploading a private file. 7 | 8 | # 3.0.1 9 | * Repair broken `FileAccessor` class. 10 | 11 | # 3.0.0 12 | * **Breaking Change**: Remove `MountableFileServer::Client`. Use `MountableFileServer::Adapter` instead. 13 | 14 | # 2.1.0 15 | * Relax Sinatra dependency so Ruby on Rails 4 can use Rack 1.X 16 | 17 | # 2.0.0 18 | * Remove HTTP endpoints for moving and deleting uploads due to security concerns. 19 | * Return 404 for unknown or malformed FIDs. 20 | 21 | # 0.0.2 - 24.08.2015 22 | * Internal refactorings. 23 | * Introduce `Adapter` class to have one point of contact. 24 | 25 | **Breaking Changes** 26 | * Files are stored under their full unique identifier in every storage. 27 | * Remove `Access` class. 28 | * Signature of `Storage` methods changed. 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in mountable_file_server.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 David Strauß 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mountable File Server can be used with any Ruby (on Rails) application and it removes the pain associated with file uploads. 2 | 3 | ## Core Idea 4 | app only stores reference to uploaded file 5 | 6 | ## MountableFileServer::Server 7 | The core of Mountable File Server (MFS) is a small HTTP API that accepts file uploads and offers endpoints to interact with uploaded files. While the frontend deals directly with the HTTP API, your Ruby application will want to use the Ruby adapter `MountableFileServer::Adapter`. 8 | 9 | 10 | 11 | 12 | 13 | 14 | MountableFileServer was born out of my frustations with existing Ruby based file upload solutions. It is certainly not comparable with existing feature-rich plug-and-play solutions that are tied to Ruby on Rails. 15 | 16 | The fundamental idea is that your application should not be tied to files or handling the upload process. Instead this is handled by a dedicated server which can be run as independet process or be mounted inside your application. It accepts file uploads and returns an unique identifier for the file. This unique identifier is basically a string and the only thing your application has to remember in order to work with uploaded files. 17 | 18 | * JavaScript function to upload files via AJAX. 19 | * JavaScript events regarding the uploads various states. 20 | * Uploaded files are stored on disk. 21 | * Filenames are unique and random sequences of charaters. 22 | * Uploaded files are stored temporary until they are explicitly moved to permanent storage by the application. 23 | * Uploaded files can be of public or private nature. 24 | * Private files can't be accessed via an URL. 25 | 26 | Things it does not do: 27 | 28 | * Image processing. Processing images has nothing to do with uploading files. [MountableImageProcessor]() is recommended for adding on the fly image processing to your application. 29 | 30 | ## Install & Setup 31 | Add the MountableFileServer gem to your Gemfile and `bundle install`. Make sure you include `vendor/assets/javascripts/mountable_file_server.js` in your frontend code. 32 | 33 | ~~~ruby 34 | gem 'mountable_file_server', '~> 0.0.2' 35 | ~~~ 36 | 37 | If your application is built upon Ruby on Rails you can add a require statement in your Gemfile. This includes an Ruby on Rails engine which allows you to require the JavaScript. 38 | 39 | ~~~ruby 40 | gem 'mountable_file_server', '~> 0.0.2', require: 'mountable_file_server/rails' 41 | ~~~ 42 | 43 | ~~~javascript 44 | //= require mountable_file_server 45 | ~~~ 46 | 47 | Once installed it is time to run the server so file uploads can be made. The server is called `MountableFileServer::Endpoint` and is a minimal Sinatra application which can be run as usual. You can also mount an endpoint inside your existing application. With Ruby on Rails this would look like this. 48 | 49 | ~~~ruby 50 | Rails.application.routes.draw do 51 | mount MountableFileServer::Endpoint, at: MountableFileServer.configuration.mounted_at 52 | end 53 | ~~~ 54 | 55 | ## Configuration 56 | As seen in the previous section there is a global configuration at `MountableFileServer.configuration` available. 57 | This is an instance of the `MountableFileServer::Configration` class. The global configuration is a default argument for all classes that require access to the configuration. In situations where you have multiple endpoints with different settings you can pass in you own configuration objects instead. 58 | 59 | The global configuration can be configured through a block. 60 | 61 | ~~~ruby 62 | MountableFileServer.configure do |configuration| 63 | configuration.mounted_at = '/uploads' 64 | configuration.stored_at = '/path/to/desired/uploads/directory' 65 | end 66 | ~~~ 67 | 68 | In order to build correct URLs for public files the `mounted_at` attribute should be set to the path where the endpoint is mounted. 69 | 70 | The `stored_at` attribute tells the MountableFileServer where to store uploaded files. It will automatically create this and all needed subdirectories. This directory should not be within the root of your webserver. Otherwise private files are accessible to the internet. 71 | 72 | ## Usage 73 | The idea is to upload a file with AJAX rather than sending it with the usual form submit. After the upload, an unique identifier (`uid`), is added to the form as a hidden element. Instead of dealing with a file upload your application only has to store the `uid`, a simple string value. 74 | 75 | Using the `MountableFileServer::Adapter` your application can work with the uploaded file, the only thing needed is the `uid`. 76 | 77 | ## JavaScript API 78 | On the JavaScript side you use `uploadFiles(element, files)` to upload files to the endpoint. The `element` argument is a DOM element that needs two data attributes. `data-endpoint` specifies where the AJAX request should be sent to, this probably matches the `mounted_at` configuration option. `data-type` tells the endpoint if the uploaded files should be treated as public or private files. Possible values are `public` or `private`. 79 | 80 | The `files` argument is an array of `File` objects or an `FileList` object. These objects are for example returned when a user selects files using an `input` element. 81 | 82 | Following events will be dispatched on the element that was passed to `uploadFiles`. When you are listening to one of these events you can access the described attributes on the `event.detail` object. 83 | 84 | `upload:start` is dispatched when the upload starts. 85 | It has the attributes `uploadId` and `file`. The `uploadId` is local and can be used to identify events in a scenario where multiple files are uploaded. The `file` attribute is the original `File` object and useful for showing a preview or other information about the file. 86 | 87 | `upload:progress` is continuously dispatched while the upload is happening. 88 | It has the attributes `uploadId` and `progress`. The `progress` attribute is the original [ProgressEvent](https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent) object of the AJAX request. 89 | 90 | `upload:success` is dispatched when the upload succeeded. 91 | It has the attributes `uploadId`, `uid` and `wasLastUpload`. The `uid` attribute is the unique identifier generated by the MountableFileServer. You will want to add it to your form and store it along your other data. The `wasLastUpload` attribute indicates if this was the last upload in progress. 92 | 93 | ## Ruby API 94 | The `MountableFileServer::Adapter` class allows you to interact with uploaded files. It takes a `MountableFileServer::Configuration` instance as argument and uses `MountableFileServer.configuration` by default. 95 | 96 | `MountableFileServer::Adapter#store_temporary(input, type, extension)` 97 | Stores the input as file in the temporary storage and returns the `uid` of the file. `input` can be a path to a file or an [IO](http://ruby-doc.org/core-2.2.2/IO.html) object. `type` can be `public` or `private` and the `extension` argument specifies the extension the file should have. 98 | 99 | `MountableFileServer::Adapter#store_permanent(input, type, extension)` 100 | Stores the input as file in the permanent storage and returns the `uid` of the file. `input` can be a path to a file or an [IO](http://ruby-doc.org/core-2.2.2/IO.html) object. `type` can be `public` or `private` and the `extension` argument specifies the extension the file should have. 101 | 102 | `MountableFileServer::Adapter#move_to_permanent_storage(uid)` 103 | Moves a file from the temporary storage to the permanent one. This is mostly used in a scenario where users upload files. A file uploaded through the endpoint is initially only stored in the temporary storage. The application has to move it explicitly to the permanent storage. For example after all validations passed. 104 | 105 | `MountableFileServer::Adapter#remove_from_permanent_storage(uid)` 106 | Removes the file from the permanent storage. 107 | 108 | `MountableFileServer::Adapter#url_for(uid)` 109 | Returns the URL for an uploaded file. Only works for public files, if you pass the `uid` of a private file an error will be raised. 110 | 111 | `MountableFileServer::Adapter#pathname_for(id)` 112 | Returns a [Pathname](http://ruby-doc.org/stdlib-2.2.2/libdoc/pathname/rdoc/Pathname.html) object for the uploaded file. The pathname will always point to the file on disk independent from the files type or current storage location. 113 | 114 | # Development 115 | Run the migrations of the Ruby on Rails dummy application to make sure you can run the tests: `cd test/rails-dummy && RAILS_ENV=test bundle exec rake db:migrate`. 116 | 117 | Run tests with `bundle exec rake test`. 118 | 119 | # Publish on RubyGems.org 120 | 121 | 1. Increment `lib/mountable_image_server/version.rb` to your liking. 122 | 2. Make a Git commit. 123 | 3. Run `bundle exec rake release`. 124 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test/" 6 | t.libs << "test/acceptance" 7 | t.pattern = "test/**/*_test.rb" 8 | end 9 | 10 | task :default => :test 11 | 12 | -------------------------------------------------------------------------------- /lib/mountable_file_server.rb: -------------------------------------------------------------------------------- 1 | require "dry-configurable" 2 | 3 | require "mountable_file_server/version" 4 | 5 | module MountableFileServer 6 | extend Dry::Configurable 7 | 8 | setting :base_url 9 | setting :storage_path 10 | end 11 | -------------------------------------------------------------------------------- /lib/mountable_file_server/adapter.rb: -------------------------------------------------------------------------------- 1 | require 'mountable_file_server/storage' 2 | require 'mountable_file_server/unique_identifier' 3 | require 'mountable_file_server/file_accessor' 4 | 5 | module MountableFileServer 6 | class Adapter 7 | attr_reader :configuration 8 | 9 | def initialize(configuration = MountableFileServer.config) 10 | @configuration = configuration 11 | end 12 | 13 | def store_temporary(input, type, extension) 14 | uid = generate_random_uid type, extension 15 | Storage.new(configuration).store_temporary uid, input 16 | uid 17 | end 18 | 19 | def store_permanent(input, type, extension) 20 | uid = generate_random_uid type, extension 21 | Storage.new(configuration).store_permanent uid, input 22 | uid 23 | end 24 | 25 | def move_to_permanent_storage(uid) 26 | uid = UniqueIdentifier.new uid 27 | Storage.new(configuration).move_to_permanent_storage uid 28 | end 29 | 30 | def remove_from_storage(uid) 31 | uid = UniqueIdentifier.new uid 32 | Storage.new(configuration).remove_from_storage uid 33 | end 34 | 35 | def url_for(uid) 36 | uid = UniqueIdentifier.new uid 37 | FileAccessor.new(uid, configuration).url 38 | end 39 | 40 | def pathname_for(uid) 41 | uid = UniqueIdentifier.new uid 42 | FileAccessor.new(uid, configuration).pathname 43 | end 44 | 45 | private 46 | def generate_random_uid(type, extension) 47 | loop do 48 | uid = UniqueIdentifier.generate_for type, extension 49 | break uid unless FileAccessor.new(uid, configuration).exist? 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/mountable_file_server/file_accessor.rb: -------------------------------------------------------------------------------- 1 | require 'mountable_file_server/uri' 2 | require 'pathname' 3 | 4 | module MountableFileServer 5 | MissingFile = Class.new(ArgumentError) 6 | NotAccessibleViaURL = Class.new(ArgumentError) 7 | 8 | class FileAccessor 9 | attr_reader :uid, :configuration 10 | 11 | def initialize(uid, configuration = MountableFileServer.config) 12 | @uid = uid 13 | @configuration = configuration 14 | end 15 | 16 | def temporary_pathname 17 | Pathname(configuration.storage_path) + 'tmp' + uid 18 | end 19 | 20 | def permanent_pathname 21 | Pathname(configuration.storage_path) + uid.type + uid 22 | end 23 | 24 | def pathname 25 | pathnames.find(-> { raise MissingFile }) { |p| p.file? } 26 | end 27 | 28 | def exist? 29 | pathnames.any? { |p| p.file? } 30 | end 31 | 32 | def url 33 | raise NotAccessibleViaURL unless uid.public? 34 | 35 | URI.new (Pathname(configuration.base_url) + uid).to_s 36 | end 37 | 38 | private 39 | def pathnames 40 | [permanent_pathname, temporary_pathname] 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/mountable_file_server/metadata.rb: -------------------------------------------------------------------------------- 1 | require 'dimensions' 2 | 3 | module MountableFileServer 4 | class Metadata 5 | attr_reader :size, :content_type, :height, :width 6 | 7 | def initialize(size:, content_type:, width: nil, height: nil) 8 | @size = size 9 | @content_type = content_type 10 | @width = width 11 | @height = height 12 | end 13 | 14 | def to_h 15 | hash = { 16 | size: size, 17 | content_type: content_type 18 | } 19 | 20 | if width && height 21 | hash = hash.merge({ 22 | height: height, 23 | width: width 24 | }) 25 | end 26 | 27 | hash 28 | end 29 | 30 | def self.for_path(path) 31 | parameters = {} 32 | 33 | parameters[:content_type] = `file --brief --mime-type #{path}`.strip 34 | parameters[:size] = File.size(path) 35 | 36 | if ['image/png', 'image/jpeg', 'image/gif', 'image/tiff'].include?(parameters[:content_type]) 37 | dimensions = Dimensions.dimensions(path) 38 | parameters[:width] = dimensions[0] 39 | parameters[:height] = dimensions[1] 40 | end 41 | 42 | new(**parameters) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/mountable_file_server/rails.rb: -------------------------------------------------------------------------------- 1 | require 'mountable_file_server' 2 | require 'mountable_file_server/server' 3 | 4 | module MountableFileServer 5 | class Engine < Rails::Engine 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/mountable_file_server/server.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | require 'pathname' 3 | 4 | require 'mountable_file_server/adapter' 5 | require 'mountable_file_server/metadata' 6 | 7 | module MountableFileServer 8 | class Server < Sinatra::Base 9 | post '/' do 10 | adapter = Adapter.new 11 | pathname = Pathname(params[:file][:tempfile].path) 12 | type = params[:type] 13 | fid = adapter.store_temporary(pathname, type, pathname.extname) 14 | url = adapter.url_for(fid) if fid.public? 15 | metadata = Metadata.for_path(pathname) 16 | 17 | content_type :json 18 | status 201 19 | 20 | { 21 | fid: fid, 22 | url: url, 23 | metadata: metadata.to_h 24 | }.to_json 25 | end 26 | 27 | get '/:fid' do |fid| 28 | begin 29 | adapter = Adapter.new 30 | pathname = adapter.pathname_for(fid) 31 | send_file pathname 32 | rescue MissingFile, MalformedIdentifier 33 | status 404 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/mountable_file_server/storage.rb: -------------------------------------------------------------------------------- 1 | module MountableFileServer 2 | class Storage 3 | attr_reader :configuration 4 | 5 | def initialize(configuration = MountableFileServer.config) 6 | @configuration = configuration 7 | end 8 | 9 | def store_temporary(uid, input) 10 | destination = FileAccessor.new(uid, configuration).temporary_pathname 11 | destination.dirname.mkpath 12 | IO.copy_stream input, destination 13 | end 14 | 15 | def store_permanent(uid, input) 16 | destination = FileAccessor.new(uid, configuration).permanent_pathname 17 | destination.dirname.mkpath 18 | IO.copy_stream input, destination 19 | end 20 | 21 | def move_to_permanent_storage(uid) 22 | source = FileAccessor.new(uid, configuration).temporary_pathname 23 | destination = FileAccessor.new(uid, configuration).permanent_pathname 24 | destination.dirname.mkpath 25 | 26 | source.rename destination 27 | end 28 | 29 | def remove_from_storage(uid) 30 | source = FileAccessor.new(uid, configuration).pathname 31 | source.delete 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/mountable_file_server/unique_identifier.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | module MountableFileServer 4 | UnknownType = Class.new(ArgumentError) 5 | MalformedIdentifier = Class.new(ArgumentError) 6 | 7 | class UniqueIdentifier < String 8 | attr_reader :type, :filename 9 | 10 | def initialize(string) 11 | raise MalformedIdentifier.new unless /(\w+)-(.+)$/.match(string) 12 | 13 | @type, @filename = /(\w+)-(.+)$/.match(string).captures 14 | 15 | raise UnknownType.new(type) unless known_type? 16 | 17 | super.freeze 18 | end 19 | 20 | def self.generate_for(type, extension) 21 | new "#{type}-#{SecureRandom.hex}#{extension}" 22 | end 23 | 24 | def public? 25 | type == 'public' 26 | end 27 | 28 | private 29 | def known_type? 30 | ['public', 'private'].include?(type) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/mountable_file_server/uri.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'pathname' 3 | 4 | module MountableFileServer 5 | class URI < String 6 | def initialize(string) 7 | super.freeze 8 | end 9 | 10 | def uid 11 | UniqueIdentifier.new filename 12 | end 13 | 14 | def filename 15 | Pathname(::URI.parse(self).path).basename.to_s 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/mountable_file_server/version.rb: -------------------------------------------------------------------------------- 1 | module MountableFileServer 2 | VERSION = "3.0.3" 3 | end 4 | -------------------------------------------------------------------------------- /mountable_file_server.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'mountable_file_server/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "mountable_file_server" 8 | spec.version = MountableFileServer::VERSION 9 | spec.authors = ["David Strauß"] 10 | spec.email = ["david@strauss.io"] 11 | spec.summary = %q{Simple mountable server that handles file uploads} 12 | spec.description = %q{} 13 | spec.homepage = "https://github.com/stravid/mountable_file_server" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 2.1" 22 | spec.add_development_dependency "rake", ">= 12.3.3" 23 | spec.add_development_dependency "minitest", "~> 5.8.0" 24 | spec.add_development_dependency "rails", "~> 5.0.7.2" 25 | spec.add_development_dependency "capybara", "2.18.0" 26 | spec.add_development_dependency "sqlite3", "~> 1.3.10" 27 | spec.add_development_dependency "poltergeist", "1.18.1" 28 | spec.add_development_dependency "rack-test", "~> 0.6.3" 29 | spec.add_development_dependency "mocha", "1.11.2" 30 | 31 | spec.add_runtime_dependency "sinatra", ">= 1.4.8" 32 | spec.add_runtime_dependency "dry-configurable", ">= 0.1.6" 33 | spec.add_runtime_dependency "dimensions", "~> 1.3.0" 34 | end 35 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import (fetchGit { 3 | url = https://github.com/NixOS/nixpkgs-channels; 4 | ref = "nixos-20.03"; 5 | }) {}, 6 | ruby ? pkgs.ruby_2_5, 7 | bundler ? pkgs.bundler.override { inherit ruby; } 8 | }: 9 | 10 | pkgs.mkShell { 11 | buildInputs = with pkgs; [ 12 | ruby 13 | bundler 14 | git 15 | sqlite 16 | zlib 17 | phantomjs 18 | rubyPackages_2_5.nokogiri 19 | libiconv 20 | ]; 21 | 22 | shellHook = '' 23 | mkdir -p .local-data/gems 24 | export GEM_HOME=$PWD/.local-data/gems 25 | export GEM_PATH=$GEM_HOME 26 | ''; 27 | } 28 | -------------------------------------------------------------------------------- /test/acceptance/form_interactions_test.rb: -------------------------------------------------------------------------------- 1 | require 'acceptance_helper' 2 | 3 | class TestFormInteractions < AcceptanceTestCase 4 | def setup 5 | MountableFileServer.configure do |config| 6 | config.base_url = "/uploads" 7 | end 8 | 9 | User.destroy_all 10 | end 11 | 12 | def test_upload_client_side_interaction 13 | visit "/users/new" 14 | attach_file("Avatar url", fixture_path('david.jpg')) 15 | 16 | sleep 0.1 17 | 18 | assert_match(/public-.*.jpg/, find('.js-mountable-file-server-input input[type=hidden]', visible: false).value) 19 | assert has_content?("Upload started.") 20 | assert has_content?("Upload succeeded.") 21 | end 22 | 23 | def test_upload 24 | visit "/users/new" 25 | fill_in "Name", with: "David" 26 | attach_file "Avatar url", fixture_path('david.jpg') 27 | 28 | sleep 0.1 29 | 30 | click_button "Create User" 31 | 32 | visit find("img")[:src] 33 | 34 | assert_equal 200, page.status_code 35 | assert_match(/^image\//, page.response_headers['Content-Type']) 36 | end 37 | 38 | def test_remove_upload 39 | visit "/users/new" 40 | fill_in "Name", with: "David" 41 | attach_file "Avatar url", fixture_path('david.jpg') 42 | 43 | sleep 0.1 44 | 45 | click_button "Create User" 46 | 47 | visit "/users" 48 | click_link "Destroy" 49 | 50 | assert_equal 200, page.status_code 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/acceptance_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | require 'minitest/autorun' 4 | require 'minitest/hell' 5 | 6 | ENV['RAILS_ENV'] ||= 'test' 7 | require File.expand_path("../rails-dummy/config/environment.rb", __FILE__) 8 | require 'capybara/rails' 9 | require 'capybara/poltergeist' 10 | require 'support/path_helper' 11 | 12 | Capybara.current_driver = :poltergeist 13 | 14 | class AcceptanceTestCase < MiniTest::Test 15 | include Capybara::DSL 16 | include PathHelper 17 | 18 | def setup 19 | end 20 | 21 | def teardown 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/fixtures/david.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgycircle/mountable_file_server/b253975eee4135df5b67dd3ee328341a5fd15c5a/test/fixtures/david.jpg -------------------------------------------------------------------------------- /test/fixtures/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgycircle/mountable_file_server/b253975eee4135df5b67dd3ee328341a5fd15c5a/test/fixtures/image.png -------------------------------------------------------------------------------- /test/fixtures/test.txt: -------------------------------------------------------------------------------- 1 | Test 2 | -------------------------------------------------------------------------------- /test/integration/server_test.rb: -------------------------------------------------------------------------------- 1 | require 'integration_helper' 2 | require 'json' 3 | 4 | class TestServer < IntegrationTestCase 5 | def setup 6 | MountableFileServer.configure do |config| 7 | config.base_url = 'http://test.test/uploads/' 8 | config.storage_path = Dir.mktmpdir 9 | end 10 | end 11 | 12 | def teardown 13 | Pathname(MountableFileServer.config.storage_path).rmtree 14 | end 15 | 16 | def test_file_upload 17 | post '/', { 18 | file: Rack::Test::UploadedFile.new(fixture_path('image.png'), 'image/jpeg'), 19 | type: 'public' 20 | } 21 | 22 | result = JSON.parse(last_response.body, symbolize_names: true) 23 | 24 | assert_equal 201, last_response.status 25 | assert_equal 'application/json', last_response.headers['Content-Type'] 26 | assert_match(/public-\w{32}\.png/, result[:fid]) 27 | assert_equal "http://test.test/uploads/#{result[:fid]}", result[:url] 28 | assert_equal File.size(fixture_path('image.png')), result[:metadata][:size] 29 | assert_equal 'image/png', result[:metadata][:content_type] 30 | assert_equal 269, result[:metadata][:width] 31 | assert_equal 234, result[:metadata][:height] 32 | end 33 | 34 | def test_private_file_upload 35 | post '/', { 36 | file: Rack::Test::UploadedFile.new(fixture_path('image.png'), 'image/jpeg'), 37 | type: 'private' 38 | } 39 | 40 | result = JSON.parse(last_response.body, symbolize_names: true) 41 | 42 | assert_equal 201, last_response.status 43 | assert_equal 'application/json', last_response.headers['Content-Type'] 44 | assert_match(/private-\w{32}\.png/, result[:fid]) 45 | assert_equal nil, result[:url] 46 | assert_equal File.size(fixture_path('image.png')), result[:metadata][:size] 47 | assert_equal 'image/png', result[:metadata][:content_type] 48 | assert_equal 269, result[:metadata][:width] 49 | assert_equal 234, result[:metadata][:height] 50 | end 51 | 52 | def test_temporary_file_download 53 | post '/', { 54 | file: Rack::Test::UploadedFile.new(fixture_path('image.png'), 'image/jpeg'), 55 | type: 'public' 56 | } 57 | 58 | fid = JSON.parse(last_response.body, symbolize_names: true)[:fid] 59 | get fid 60 | 61 | Dir.mktmpdir('downloads') do |dir| 62 | File.open(File.join(dir, 'test.png'), 'wb') { |file| file.write(last_response.body) } 63 | assert_equal Pathname(fixture_path('image.png')).read, Pathname(File.join(dir, 'test.png')).read 64 | end 65 | end 66 | 67 | def test_download_of_unknown_fid 68 | fid = 'public-unknown.png' 69 | get fid 70 | 71 | assert_equal 404, last_response.status 72 | end 73 | 74 | def test_download_of_malformed_fid 75 | fid = 'bla.png' 76 | get fid 77 | 78 | assert_equal 404, last_response.status 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/integration_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | require 'minitest/autorun' 4 | require 'minitest/hell' 5 | require 'rack/test' 6 | require 'support/path_helper' 7 | require 'mountable_file_server' 8 | require 'mountable_file_server/server' 9 | 10 | class IntegrationTestCase < MiniTest::Test 11 | include Rack::Test::Methods 12 | include PathHelper 13 | 14 | def app 15 | @app = MountableFileServer::Server.new 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/rails-dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /test/rails-dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/rails-dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link application.css 2 | //= link application.js 3 | -------------------------------------------------------------------------------- /test/rails-dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgycircle/mountable_file_server/b253975eee4135df5b67dd3ee328341a5fd15c5a/test/rails-dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /test/rails-dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require mountable_file_server 14 | //= require_tree . 15 | 16 | document.addEventListener('DOMContentLoaded', function() { 17 | var form = document.querySelector("form"); 18 | var input = document.querySelector("input[type=file]"); 19 | 20 | if (!form || !input) { return; } 21 | 22 | var uploader = new MountableFileServerUploader({ 23 | url: '/uploads' 24 | }); 25 | 26 | input.addEventListener('change', function(event) { 27 | var self = this; 28 | 29 | for (var i = 0; i < this.files.length; i++) { 30 | uploader.uploadFile({ 31 | file: this.files[i], 32 | type: 'public', 33 | onStart: function() { 34 | console.log('onStart'); 35 | 36 | var p = document.createElement("p"); 37 | p.textContent = "Upload started."; 38 | form.appendChild(p); 39 | }, 40 | onProgress: function(progress) { 41 | console.log('onProgress', progress); 42 | 43 | var p = document.createElement("p"); 44 | p.textContent = "Upload progess " + progress.loaded + " of " + progress.total + "."; 45 | form.appendChild(p); 46 | }, 47 | onSuccess: function(response) { 48 | console.log('onSuccess', response); 49 | 50 | var $hiddenInput = document.createElement('input'); 51 | 52 | $hiddenInput.value = response.fid; 53 | $hiddenInput.name = self.name; 54 | $hiddenInput.type = 'hidden'; 55 | self.parentNode.insertBefore($hiddenInput, self.nextSibling); 56 | 57 | var p = document.createElement("p"); 58 | p.textContent = "Upload succeeded."; 59 | form.appendChild(p); 60 | } 61 | }); 62 | } 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/rails-dummy/app/assets/javascripts/users.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /test/rails-dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /test/rails-dummy/app/assets/stylesheets/scaffold.css: -------------------------------------------------------------------------------- 1 | body { background-color: #fff; color: #333; } 2 | 3 | body, p, ol, ul, td { 4 | font-family: verdana, arial, helvetica, sans-serif; 5 | font-size: 13px; 6 | line-height: 18px; 7 | } 8 | 9 | pre { 10 | background-color: #eee; 11 | padding: 10px; 12 | font-size: 11px; 13 | } 14 | 15 | a { color: #000; } 16 | a:visited { color: #666; } 17 | a:hover { color: #fff; background-color:#000; } 18 | 19 | div.field, div.actions { 20 | margin-bottom: 10px; 21 | } 22 | 23 | #notice { 24 | color: green; 25 | } 26 | 27 | .field_with_errors { 28 | padding: 2px; 29 | background-color: red; 30 | display: table; 31 | } 32 | 33 | #error_explanation { 34 | width: 450px; 35 | border: 2px solid red; 36 | padding: 7px; 37 | padding-bottom: 0; 38 | margin-bottom: 20px; 39 | background-color: #f0f0f0; 40 | } 41 | 42 | #error_explanation h2 { 43 | text-align: left; 44 | font-weight: bold; 45 | padding: 5px 5px 5px 15px; 46 | font-size: 12px; 47 | margin: -7px; 48 | margin-bottom: 0px; 49 | background-color: #c00; 50 | color: #fff; 51 | } 52 | 53 | #error_explanation ul li { 54 | font-size: 12px; 55 | list-style: square; 56 | } 57 | -------------------------------------------------------------------------------- /test/rails-dummy/app/assets/stylesheets/users.css: -------------------------------------------------------------------------------- 1 | /* 2 | Place all the styles related to the matching controller here. 3 | They will automatically be included in application.css. 4 | */ 5 | -------------------------------------------------------------------------------- /test/rails-dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /test/rails-dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgycircle/mountable_file_server/b253975eee4135df5b67dd3ee328341a5fd15c5a/test/rails-dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /test/rails-dummy/app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | before_action :set_user, only: [:show, :edit, :update, :destroy] 3 | 4 | # GET /users 5 | def index 6 | @users = User.all 7 | end 8 | 9 | # GET /users/1 10 | def show 11 | @adapter = MountableFileServer::Adapter.new 12 | end 13 | 14 | # GET /users/new 15 | def new 16 | @user = User.new 17 | end 18 | 19 | # GET /users/1/edit 20 | def edit 21 | end 22 | 23 | # POST /users 24 | def create 25 | @user = User.new(user_params) 26 | 27 | if @user.save 28 | adapter = MountableFileServer::Adapter.new 29 | adapter.move_to_permanent_storage @user.avatar_url 30 | redirect_to @user, notice: 'User was successfully created.' 31 | else 32 | render :new 33 | end 34 | end 35 | 36 | # PATCH/PUT /users/1 37 | def update 38 | if @user.update(user_params) 39 | redirect_to @user, notice: 'User was successfully updated.' 40 | else 41 | render :edit 42 | end 43 | end 44 | 45 | # DELETE /users/1 46 | def destroy 47 | adapter = MountableFileServer::Adapter.new 48 | adapter.remove_from_storage @user.avatar_url 49 | @user.destroy 50 | redirect_to users_url, notice: 'User was successfully destroyed.' 51 | end 52 | 53 | private 54 | # Use callbacks to share common setup or constraints between actions. 55 | def set_user 56 | @user = User.find(params[:id]) 57 | end 58 | 59 | # Only allow a trusted parameter "white list" through. 60 | def user_params 61 | params.require(:user).permit(:name, :avatar_url) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/rails-dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/rails-dummy/app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | module UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/rails-dummy/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgycircle/mountable_file_server/b253975eee4135df5b67dd3ee328341a5fd15c5a/test/rails-dummy/app/mailers/.keep -------------------------------------------------------------------------------- /test/rails-dummy/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgycircle/mountable_file_server/b253975eee4135df5b67dd3ee328341a5fd15c5a/test/rails-dummy/app/models/.keep -------------------------------------------------------------------------------- /test/rails-dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgycircle/mountable_file_server/b253975eee4135df5b67dd3ee328341a5fd15c5a/test/rails-dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /test/rails-dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/rails-dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/rails-dummy/app/views/users/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(@user) do |f| %> 2 | <% if @user.errors.any? %> 3 |
4 |

<%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:

5 | 6 | 11 |
12 | <% end %> 13 | 14 |
15 | <%= f.label :name %>
16 | <%= f.text_field :name %> 17 |
18 | 19 |
20 | <%= f.label :avatar_url %>
21 |
22 | <%= f.text_field :avatar_url, type: :file, data: { endpoint: '/uploads', type: 'public' } %> 23 |
24 |
25 | 26 |
27 | <%= f.submit %> 28 |
29 | <% end %> 30 | -------------------------------------------------------------------------------- /test/rails-dummy/app/views/users/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Editing user

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Show', @user %> | 6 | <%= link_to 'Back', users_path %> 7 | -------------------------------------------------------------------------------- /test/rails-dummy/app/views/users/index.html.erb: -------------------------------------------------------------------------------- 1 |

Listing users

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <% @users.each do |user| %> 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | <% end %> 22 | 23 |
NameAvatar url
<%= user.name %><%= user.avatar_url %><%= link_to 'Show', user %><%= link_to 'Edit', edit_user_path(user) %><%= link_to 'Destroy', destroy_user_path(user) %>
24 | 25 |
26 | 27 | <%= link_to 'New User', new_user_path %> 28 | -------------------------------------------------------------------------------- /test/rails-dummy/app/views/users/new.html.erb: -------------------------------------------------------------------------------- 1 |

New user

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Back', users_path %> 6 | -------------------------------------------------------------------------------- /test/rails-dummy/app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 |

<%= notice %>

2 | 3 |

4 | Name: 5 | <%= @user.name %> 6 |

7 | 8 |

9 | Avatar: 10 | <%= image_tag @adapter.url_for(@user.avatar_url) %> 11 |

12 | 13 | <%= link_to 'Edit', edit_user_path(@user) %> | 14 | <%= link_to 'Back', users_path %> 15 | -------------------------------------------------------------------------------- /test/rails-dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /test/rails-dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /test/rails-dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/rails-dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /test/rails-dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "mountable_file_server/rails" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Settings in config/environments/* take precedence over those specified here. 11 | # Application configuration should go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded. 13 | 14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 16 | # config.time_zone = 'Central Time (US & Canada)' 17 | 18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 20 | # config.i18n.default_locale = :de 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /test/rails-dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /test/rails-dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /test/rails-dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/rails-dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Adds additional error checking when serving assets at runtime. 31 | # Checks for improperly declared sprockets dependencies. 32 | # Raises helpful error messages. 33 | config.assets.raise_runtime_errors = true 34 | 35 | # Raises error for missing translations 36 | # config.action_view.raise_on_missing_translations = true 37 | end 38 | -------------------------------------------------------------------------------- /test/rails-dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_files = false 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 36 | 37 | # Specifies the header that your server uses for sending files. 38 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 40 | 41 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 42 | # config.force_ssl = true 43 | 44 | # Set to :debug to see everything in the log. 45 | config.log_level = :info 46 | 47 | # Prepend all log lines with the following tags. 48 | # config.log_tags = [ :subdomain, :uuid ] 49 | 50 | # Use a different logger for distributed setups. 51 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 52 | 53 | # Use a different cache store in production. 54 | # config.cache_store = :mem_cache_store 55 | 56 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 57 | # config.action_controller.asset_host = "http://assets.example.com" 58 | 59 | # Ignore bad email addresses and do not raise email delivery errors. 60 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 61 | # config.action_mailer.raise_delivery_errors = false 62 | 63 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 64 | # the I18n.default_locale when a translation cannot be found). 65 | config.i18n.fallbacks = true 66 | 67 | # Send deprecation notices to registered listeners. 68 | config.active_support.deprecation = :notify 69 | 70 | # Disable automatic flushing of the log to improve performance. 71 | # config.autoflush_log = false 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | end 79 | -------------------------------------------------------------------------------- /test/rails-dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | # Raises error for missing translations 38 | # config.action_view.raise_on_missing_translations = true 39 | end 40 | -------------------------------------------------------------------------------- /test/rails-dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Precompile additional assets. 7 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 8 | # Rails.application.config.assets.precompile += %w( search.js ) 9 | -------------------------------------------------------------------------------- /test/rails-dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test/rails-dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json -------------------------------------------------------------------------------- /test/rails-dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /test/rails-dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /test/rails-dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /test/rails-dummy/config/initializers/mountable_file_server.rb: -------------------------------------------------------------------------------- 1 | MountableFileServer.configure do |config| 2 | config.base_url = '/uploads/' 3 | 4 | if Rails.env.test? 5 | config.storage_path = File.join(Rails.root, 'tmp', 'test-uploads') 6 | else 7 | config.storage_path = File.join(Rails.root, 'uploads') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/rails-dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /test/rails-dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /test/rails-dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /test/rails-dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :users do 3 | get :destroy, on: :member, as: :destroy, path: 'destroy' 4 | end 5 | 6 | mount MountableFileServer::Server, at: '/uploads' 7 | end 8 | -------------------------------------------------------------------------------- /test/rails-dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 63134486bdad8cb685c5e3a6da334a8ce3110934c63d7aebe5e0b2253036a74a33bfc06c068bce6fc79700ee2289deabe2cdee0e4a60c99f4de6e9bbc1906f8c 15 | 16 | test: 17 | secret_key_base: c1cc8edcb033db8be1ba51970c6b93886e8a2ec56d3735b3952e42b1a448b76bf9c65a97c5ccdda668bfcc25d0a437cacd6e92c460fe2c9f03145d20cf222e1e 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /test/rails-dummy/db/migrate/20150305204311_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.string :name 5 | t.string :avatar_url 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/rails-dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20150305204311) do 14 | 15 | create_table "users", force: :cascade do |t| 16 | t.string "name" 17 | t.string "avatar_url" 18 | t.datetime "created_at" 19 | t.datetime "updated_at" 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /test/rails-dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgycircle/mountable_file_server/b253975eee4135df5b67dd3ee328341a5fd15c5a/test/rails-dummy/lib/assets/.keep -------------------------------------------------------------------------------- /test/rails-dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgycircle/mountable_file_server/b253975eee4135df5b67dd3ee328341a5fd15c5a/test/rails-dummy/log/.keep -------------------------------------------------------------------------------- /test/rails-dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/rails-dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /test/rails-dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /test/rails-dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgycircle/mountable_file_server/b253975eee4135df5b67dd3ee328341a5fd15c5a/test/rails-dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/support/path_helper.rb: -------------------------------------------------------------------------------- 1 | module PathHelper 2 | def fixture_path(filename) 3 | File.expand_path(File.join('../fixtures', filename), File.dirname(__FILE__)) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/unit/adapter_test.rb: -------------------------------------------------------------------------------- 1 | require 'unit_helper' 2 | require 'mocha/minitest' 3 | require 'stringio' 4 | require 'pathname' 5 | 6 | class AdapterTest < UnitTestCase 7 | UniqueIdentifier = MountableFileServer::UniqueIdentifier 8 | Adapter = MountableFileServer::Adapter 9 | 10 | class Configuration 11 | attr_reader :base_url, :storage_path 12 | 13 | def initialize(base_url, storage_path = '') 14 | @base_url = base_url 15 | @storage_path = storage_path 16 | end 17 | end 18 | 19 | def setup 20 | @stored_at = Dir.mktmpdir 21 | end 22 | 23 | def teardown 24 | Pathname(@stored_at).rmtree 25 | end 26 | 27 | def test_uid_within_directories_when_storing_temporary 28 | io = StringIO.new 'test' 29 | adapter = Adapter.new configuration 30 | random_identifiers = [ 31 | UniqueIdentifier.new('public-a.txt'), 32 | UniqueIdentifier.new('public-a.txt'), 33 | UniqueIdentifier.new('public-b.txt') 34 | ] 35 | UniqueIdentifier.stubs(:generate_for).returns(*random_identifiers) 36 | 37 | identifier_a = adapter.store_temporary io, 'public', '.txt' 38 | identifier_b = adapter.store_temporary io, 'public', '.txt' 39 | 40 | assert_equal 'public-a.txt', identifier_a 41 | assert_equal 'public-b.txt', identifier_b 42 | end 43 | 44 | def test_uid_within_directories_when_storing_permanent 45 | io = StringIO.new 'test' 46 | adapter = Adapter.new configuration 47 | random_identifiers = [ 48 | UniqueIdentifier.new('public-a.txt'), 49 | UniqueIdentifier.new('public-a.txt'), 50 | UniqueIdentifier.new('public-b.txt') 51 | ] 52 | UniqueIdentifier.stubs(:generate_for).returns(*random_identifiers) 53 | 54 | identifier_a = adapter.store_permanent io, 'public', '.txt' 55 | identifier_b = adapter.store_permanent io, 'public', '.txt' 56 | 57 | assert_equal 'public-a.txt', identifier_a 58 | assert_equal 'public-b.txt', identifier_b 59 | end 60 | 61 | def test_url_of_public_uid_is_returned 62 | configuration = Configuration.new '/abc' 63 | 64 | [ 65 | 'public-test.png', 66 | UniqueIdentifier.new('public-test.png') 67 | ].each do |uid| 68 | assert_equal '/abc/public-test.png', Adapter.new(configuration).url_for(uid) 69 | end 70 | end 71 | 72 | def test_private_uids_do_not_have_urls 73 | [ 74 | 'private-test.png', 75 | UniqueIdentifier.new('private-test.png') 76 | ].each do |uid| 77 | assert_raises(MountableFileServer::NotAccessibleViaURL) do 78 | Adapter.new.url_for(uid) 79 | end 80 | end 81 | end 82 | 83 | def test_path_for_object_uid_is_returned 84 | [ 85 | { directory: 'tmp', filename: 'public-' }, 86 | { directory: 'tmp', filename: 'private-' }, 87 | { directory: 'public', filename: 'public-' }, 88 | { directory: 'private', filename: 'private-' } 89 | ].each do |combination| 90 | Dir.mktmpdir do |directory| 91 | path = Pathname(directory) + combination[:directory] 92 | path.mkpath 93 | configuration = Configuration.new '', directory 94 | 95 | Tempfile.open(combination[:filename], path) do |file| 96 | uid = UniqueIdentifier.new File.basename(file) 97 | 98 | assert_equal Pathname(file.path), Adapter.new(configuration).pathname_for(uid) 99 | end 100 | end 101 | end 102 | end 103 | 104 | def test_path_for_string_uid_is_returned 105 | [ 106 | { directory: 'tmp', filename: 'public-' }, 107 | { directory: 'tmp', filename: 'private-' }, 108 | { directory: 'public', filename: 'public-' }, 109 | { directory: 'private', filename: 'private-' } 110 | ].each do |combination| 111 | Dir.mktmpdir do |directory| 112 | path = Pathname(directory) + combination[:directory] 113 | path.mkpath 114 | configuration = Configuration.new '', directory 115 | 116 | Tempfile.open(combination[:filename], path) do |file| 117 | uid = File.basename(file) 118 | 119 | assert_equal Pathname(file.path), Adapter.new(configuration).pathname_for(uid) 120 | end 121 | end 122 | end 123 | end 124 | 125 | private 126 | def configuration 127 | Configuration.new '', @stored_at 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /test/unit/file_accessor_test.rb: -------------------------------------------------------------------------------- 1 | require 'unit_helper' 2 | require 'tempfile' 3 | require 'pathname' 4 | 5 | require 'mountable_file_server' 6 | require 'mountable_file_server/uri' 7 | 8 | class FileAccessorTest < UnitTestCase 9 | UniqueIdentifier = MountableFileServer::UniqueIdentifier 10 | FileAccessor = MountableFileServer::FileAccessor 11 | URI = MountableFileServer::URI 12 | 13 | class Configuration 14 | attr_reader :base_url, :storage_path 15 | 16 | def initialize(base_url, storage_path = '') 17 | @base_url = base_url 18 | @storage_path = storage_path 19 | end 20 | end 21 | 22 | def test_temporary_pathname 23 | configuration = Configuration.new '', '/some/path/' 24 | 25 | [ 26 | { uid: 'public-test.jpg', path: '/some/path/tmp/public-test.jpg' }, 27 | { uid: 'private-test.jpg', path: '/some/path/tmp/private-test.jpg' } 28 | ].each do |pair| 29 | file_acccessor = FileAccessor.new UniqueIdentifier.new(pair[:uid]), configuration 30 | assert_equal Pathname(pair[:path]), file_acccessor.temporary_pathname 31 | end 32 | end 33 | 34 | def test_permanent_pathname 35 | configuration = Configuration.new '', '/some/path/' 36 | 37 | [ 38 | { uid: 'public-test.jpg', path: '/some/path/public/public-test.jpg' }, 39 | { uid: 'private-test.jpg', path: '/some/path/private/private-test.jpg' } 40 | ].each do |pair| 41 | file_acccessor = FileAccessor.new UniqueIdentifier.new(pair[:uid]), configuration 42 | assert_equal Pathname(pair[:path]), file_acccessor.permanent_pathname 43 | end 44 | end 45 | 46 | def test_finds_pathname_based_on_file_location 47 | [ 48 | { uid: 'public-test.jpg', location: 'public/public-test.jpg' }, 49 | { uid: 'public-test.jpg', location: 'tmp/public-test.jpg' }, 50 | { uid: 'private-test.jpg', location: 'private/private-test.jpg' }, 51 | { uid: 'private-test.jpg', location: 'tmp/private-test.jpg' } 52 | ].each do |pair| 53 | Dir.mktmpdir do |directory| 54 | stored_at = Pathname(directory) 55 | configuration = Configuration.new '', stored_at 56 | pathname = stored_at + pair[:location] 57 | pathname.dirname.mkdir 58 | 59 | File.open(pathname, 'w') { |f| f.write 'test' } 60 | 61 | file_acccessor = FileAccessor.new UniqueIdentifier.new(pair[:uid]), configuration 62 | assert_equal pathname, file_acccessor.pathname 63 | end 64 | end 65 | end 66 | 67 | def test_pathname_raises_error_when_no_file_is_present 68 | uid = UniqueIdentifier.new 'public-unknown.jpg' 69 | file_acccessor = FileAccessor.new uid 70 | 71 | assert_raises(MountableFileServer::MissingFile) { file_acccessor.pathname } 72 | end 73 | 74 | def test_exist_returns_false_when_no_file_is_present 75 | uid = UniqueIdentifier.new 'public-unknown.jpg' 76 | refute FileAccessor.new(uid).exist? 77 | end 78 | 79 | def test_exist_checks_all_possible_file_locations 80 | [ 81 | { uid: 'public-test.jpg', location: 'public/public-test.jpg' }, 82 | { uid: 'public-test.jpg', location: 'tmp/public-test.jpg' }, 83 | { uid: 'private-test.jpg', location: 'private/private-test.jpg' }, 84 | { uid: 'private-test.jpg', location: 'tmp/private-test.jpg' } 85 | ].each do |pair| 86 | Dir.mktmpdir do |directory| 87 | stored_at = Pathname(directory) 88 | configuration = Configuration.new '', stored_at 89 | pathname = stored_at + pair[:location] 90 | pathname.dirname.mkdir 91 | 92 | File.open(pathname, 'w') { |f| f.write 'test' } 93 | 94 | file_acccessor = FileAccessor.new UniqueIdentifier.new(pair[:uid]), configuration 95 | assert file_acccessor.exist? 96 | end 97 | end 98 | end 99 | 100 | def test_public_uid_has_an_url 101 | configuration = Configuration.new '/abc' 102 | uid = UniqueIdentifier.new 'public-test.png' 103 | file_acccessor = FileAccessor.new uid, configuration 104 | 105 | assert_equal '/abc/public-test.png', file_acccessor.url 106 | assert_instance_of URI, file_acccessor.url 107 | end 108 | 109 | def test_private_uid_has_no_url 110 | uid = UniqueIdentifier.new 'private-test.png' 111 | file_acccessor = FileAccessor.new uid 112 | 113 | assert_raises(MountableFileServer::NotAccessibleViaURL) do 114 | file_acccessor.url 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/unit/metadata_test.rb: -------------------------------------------------------------------------------- 1 | require 'unit_helper' 2 | 3 | require 'mountable_file_server/metadata' 4 | 5 | class MetadataTest < UnitTestCase 6 | Metadata = MountableFileServer::Metadata 7 | 8 | def test_size 9 | subject = Metadata.for_path(fixture_path('image.png')) 10 | result = subject.size 11 | assert_equal File.size(fixture_path('image.png')), result 12 | end 13 | 14 | def test_content_type 15 | subject = Metadata.for_path(fixture_path('image.png')) 16 | result = subject.content_type 17 | assert_equal 'image/png', result 18 | end 19 | 20 | def test_image_height 21 | subject = Metadata.for_path(fixture_path('image.png')) 22 | result = subject.height 23 | assert_equal 234, result 24 | end 25 | 26 | def test_image_width 27 | subject = Metadata.for_path(fixture_path('image.png')) 28 | result = subject.width 29 | assert_equal 269, result 30 | end 31 | 32 | def test_image_hash 33 | subject = Metadata.for_path(fixture_path('image.png')) 34 | result = subject.to_h 35 | assert_equal({ 36 | content_type: 'image/png', 37 | size: File.size(fixture_path('image.png')), 38 | height: 234, 39 | width: 269 40 | }, result) 41 | end 42 | 43 | def test_non_image_hash 44 | subject = Metadata.for_path(fixture_path('test.txt')) 45 | result = subject.to_h 46 | assert_equal({ 47 | content_type: 'text/plain', 48 | size: File.size(fixture_path('test.txt')) 49 | }, result) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/unit/storage_test.rb: -------------------------------------------------------------------------------- 1 | require 'unit_helper' 2 | require 'stringio' 3 | require 'tempfile' 4 | 5 | require 'mountable_file_server/storage' 6 | require 'mountable_file_server/unique_identifier' 7 | require 'mountable_file_server/file_accessor' 8 | 9 | class StorageTest < UnitTestCase 10 | attr_reader :uid, :file_accessor, :storage 11 | 12 | Storage = MountableFileServer::Storage 13 | UniqueIdentifier = MountableFileServer::UniqueIdentifier 14 | FileAccessor = MountableFileServer::FileAccessor 15 | 16 | def setup 17 | MountableFileServer.configure do |config| 18 | config.base_url = 'http://test.test/uploads/' 19 | config.storage_path = Dir.mktmpdir 20 | end 21 | 22 | @uid = UniqueIdentifier.new 'public-test.txt' 23 | @file_accessor = FileAccessor.new uid 24 | @storage = Storage.new 25 | end 26 | 27 | def teardown 28 | Pathname(MountableFileServer.config.storage_path).rmtree 29 | end 30 | 31 | def test_store_io_input_temporary 32 | storage.store_temporary uid, StringIO.new('test') 33 | assert_equal 'test', file_accessor.temporary_pathname.read 34 | end 35 | 36 | def test_store_io_input_permanent 37 | storage.store_permanent uid, StringIO.new('test') 38 | assert_equal 'test', file_accessor.permanent_pathname.read 39 | end 40 | 41 | def test_store_pathname_input_temporary 42 | Tempfile.open('input') do |file| 43 | file.write 'test' 44 | file.rewind 45 | 46 | storage.store_temporary uid, file.path 47 | assert_equal 'test', file_accessor.temporary_pathname.read 48 | end 49 | end 50 | 51 | def test_store_pathname_input_permanent 52 | Tempfile.open('input') do |file| 53 | file.write 'test' 54 | file.rewind 55 | 56 | storage.store_permanent uid, file.path 57 | assert_equal 'test', file_accessor.permanent_pathname.read 58 | end 59 | end 60 | 61 | def test_move_to_permanent_storage 62 | temporary_pathname = file_accessor.temporary_pathname 63 | temporary_pathname.dirname.mkpath 64 | 65 | File.open(temporary_pathname, 'w') { |f| f.write 'test' } 66 | 67 | storage.move_to_permanent_storage uid 68 | 69 | refute file_accessor.temporary_pathname.file? 70 | assert file_accessor.permanent_pathname.file? 71 | assert_equal 'test', file_accessor.permanent_pathname.read 72 | end 73 | 74 | def test_remove_from_permanent_storage 75 | pathname = file_accessor.permanent_pathname 76 | pathname.dirname.mkpath 77 | 78 | File.open(pathname, 'w') { |f| f.write 'test' } 79 | 80 | storage.remove_from_storage uid 81 | 82 | refute file_accessor.permanent_pathname.file? 83 | end 84 | 85 | def test_remove_from_temporary_storage 86 | pathname = file_accessor.temporary_pathname 87 | pathname.dirname.mkpath 88 | 89 | File.open(pathname, 'w') { |f| f.write 'test' } 90 | 91 | storage.remove_from_storage uid 92 | 93 | refute file_accessor.temporary_pathname.file? 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/unit/unique_identifier_test.rb: -------------------------------------------------------------------------------- 1 | require 'unit_helper' 2 | 3 | class UniqueIdentifierTest < UnitTestCase 4 | UniqueIdentifier = MountableFileServer::UniqueIdentifier 5 | 6 | def test_generate_uid_for_extension 7 | uid = UniqueIdentifier.generate_for 'public', '.png' 8 | assert_match(/\w+\.png$/, uid) 9 | end 10 | 11 | def test_generate_public_uid 12 | uid = UniqueIdentifier.generate_for 'public', '.png' 13 | assert_match(/^public-\w+/, uid) 14 | end 15 | 16 | def test_generate_private_uid 17 | uid = UniqueIdentifier.generate_for 'private', '.png' 18 | assert_match(/^private-\w+/, uid) 19 | end 20 | 21 | def test_generate_accepts_only_known_types 22 | assert_raises(MountableFileServer::UnknownType) do 23 | UniqueIdentifier.generate_for 'unknow', '.png' 24 | end 25 | end 26 | 27 | def test_generate_returns_new_uid 28 | assert_instance_of UniqueIdentifier, UniqueIdentifier.generate_for('public', '.png') 29 | end 30 | 31 | def test_instantiation_with_string 32 | assert UniqueIdentifier.new 'public-test.png' 33 | end 34 | 35 | def test_instantiation_with_uid 36 | assert UniqueIdentifier.new UniqueIdentifier.new('public-test.png') 37 | end 38 | 39 | def test_instantiation_accepts_only_known_types 40 | assert_raises(MountableFileServer::UnknownType) do 41 | UniqueIdentifier.new 'unknown-test.png' 42 | end 43 | end 44 | 45 | def test_knows_if_uid_is_public 46 | assert UniqueIdentifier.new('public-test.png').public? 47 | refute UniqueIdentifier.new('private-test.png').public? 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/unit/uri_test.rb: -------------------------------------------------------------------------------- 1 | require 'unit_helper' 2 | 3 | class URITest < UnitTestCase 4 | URI = MountableFileServer::URI 5 | UniqueIdentifier = MountableFileServer::UniqueIdentifier 6 | 7 | def test_acts_like_a_string 8 | uri = URI.new '/uploads/public-test.jpg' 9 | assert_equal '/uploads/public-test.jpg', uri 10 | end 11 | 12 | def test_extract_uid 13 | uri = URI.new '/uploads/public-test.jpg' 14 | 15 | assert_instance_of UniqueIdentifier, uri.uid 16 | assert_equal 'public-test.jpg', uri.uid 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/unit_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | require 'minitest/autorun' 4 | require 'minitest/hell' 5 | require 'support/path_helper' 6 | require 'mountable_file_server' 7 | 8 | class UnitTestCase < MiniTest::Test 9 | include PathHelper 10 | end 11 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/mountable_file_server.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var MountableFileServerUploader = function(options) { 3 | return { 4 | url: options.url, 5 | uploadFile: function(options) { 6 | 7 | var file = options.file; 8 | var type = options.type; 9 | var onStart = options.onStart || function() {}; 10 | var onProgress = options.onProgress || function() {}; 11 | var onSuccess = options.onSuccess || function() {}; 12 | var xhr = new XMLHttpRequest(); 13 | var formData = new FormData(); 14 | 15 | formData.append('file', file); 16 | formData.append('type', type); 17 | 18 | xhr.open('POST', this.url, true); 19 | 20 | xhr.onreadystatechange = function() { 21 | if (xhr.readyState === 4 && xhr.status === 201) { 22 | var response = JSON.parse(xhr.responseText); 23 | 24 | response.original_filename = file.name; 25 | 26 | onSuccess(response); 27 | } 28 | } 29 | 30 | xhr.upload.addEventListener('progress', function(progressEvent) { 31 | if (progressEvent.lengthComputable) { 32 | onProgress(progressEvent); 33 | } 34 | }); 35 | 36 | xhr.send(formData); 37 | 38 | onStart(); 39 | } 40 | }; 41 | }; 42 | 43 | window.MountableFileServerUploader = MountableFileServerUploader; 44 | })(); 45 | --------------------------------------------------------------------------------