├── docs ├── ssh_deployment │ └── app │ │ └── app.rb ├── s3_backup │ └── app │ │ └── files │ │ └── bos.png ├── site │ ├── public │ │ ├── fonts │ │ │ ├── aller-bold.eot │ │ │ ├── aller-bold.ttf │ │ │ ├── aller-bold.woff │ │ │ ├── aller-light.eot │ │ │ ├── aller-light.ttf │ │ │ ├── aller-light.woff │ │ │ ├── novecento-bold.eot │ │ │ ├── novecento-bold.ttf │ │ │ └── novecento-bold.woff │ │ └── stylesheets │ │ │ └── normalize.css │ ├── index.html │ ├── 404.html │ ├── s3_basics.html │ ├── ssh_basics.html │ ├── s3_sandbox.html │ ├── ssh_sandbox.html │ ├── s3_backup.html │ ├── ssh_deployment.html │ ├── docco.css │ └── basics.html ├── s3_basics.rb ├── ssh_basics.rb ├── s3_sandbox.rb ├── ssh_sandbox.rb ├── s3_backup.rb ├── ssh_deployment.rb └── basics.rb ├── spec ├── storages │ ├── local_spec │ │ └── emptygit │ └── local_spec.rb ├── misc_spec.rb ├── spec_helper.rb ├── entry_spec.rb ├── container_spec.rb ├── universal_entry_spec.rb ├── path_spec.rb ├── file_spec.rb └── dir_spec.rb ├── lib ├── vfs │ ├── error.rb │ ├── entries │ │ ├── universal_entry.rb │ │ ├── entry.rb │ │ ├── file.rb │ │ └── dir.rb │ ├── integration.rb │ ├── vfs.rb │ ├── entry_proxy.rb │ ├── path.rb │ └── drivers │ │ ├── local.rb │ │ └── specification.rb └── vfs.rb ├── .gitignore ├── old ├── hash_fs_spec.rb └── hash_fs.rb ├── gemspec ├── todo.md └── readme.md /docs/ssh_deployment/app/app.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/storages/local_spec/emptygit: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/vfs/error.rb: -------------------------------------------------------------------------------- 1 | module Vfs 2 | class Error < StandardError 3 | end 4 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/* 2 | spec/config.yml 3 | 4 | *~ 5 | .DS_Store 6 | .idea 7 | .idea/**/* 8 | Thumbs.db -------------------------------------------------------------------------------- /docs/s3_backup/app/files/bos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al6x/vfs/HEAD/docs/s3_backup/app/files/bos.png -------------------------------------------------------------------------------- /docs/site/public/fonts/aller-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al6x/vfs/HEAD/docs/site/public/fonts/aller-bold.eot -------------------------------------------------------------------------------- /docs/site/public/fonts/aller-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al6x/vfs/HEAD/docs/site/public/fonts/aller-bold.ttf -------------------------------------------------------------------------------- /docs/site/public/fonts/aller-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al6x/vfs/HEAD/docs/site/public/fonts/aller-bold.woff -------------------------------------------------------------------------------- /docs/site/public/fonts/aller-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al6x/vfs/HEAD/docs/site/public/fonts/aller-light.eot -------------------------------------------------------------------------------- /docs/site/public/fonts/aller-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al6x/vfs/HEAD/docs/site/public/fonts/aller-light.ttf -------------------------------------------------------------------------------- /docs/site/public/fonts/aller-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al6x/vfs/HEAD/docs/site/public/fonts/aller-light.woff -------------------------------------------------------------------------------- /docs/site/public/fonts/novecento-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al6x/vfs/HEAD/docs/site/public/fonts/novecento-bold.eot -------------------------------------------------------------------------------- /docs/site/public/fonts/novecento-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al6x/vfs/HEAD/docs/site/public/fonts/novecento-bold.ttf -------------------------------------------------------------------------------- /docs/site/public/fonts/novecento-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/al6x/vfs/HEAD/docs/site/public/fonts/novecento-bold.woff -------------------------------------------------------------------------------- /docs/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Please follow this link.

7 | 8 | -------------------------------------------------------------------------------- /docs/site/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Page not exists, redirecting to home.

7 | 8 | -------------------------------------------------------------------------------- /old/hash_fs_spec.rb: -------------------------------------------------------------------------------- 1 | require 'vfs/drivers/hash_fs' 2 | require 'vfs/drivers/specification' 3 | 4 | describe Vfs::Drivers::HashFs do 5 | it_should_behave_like "vfs driver" 6 | 7 | before do 8 | @driver = Vfs::Drivers::HashFs.new 9 | end 10 | end -------------------------------------------------------------------------------- /gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'vfs' 3 | s.version = '0.5.1' 4 | s.summary = "Virtual File System - simple and unified API over different storages (Local, S3, SFTP, ...)" 5 | s.files = Dir.glob('lib/**/*.rb') 6 | s.authors = ["Alexey Petrushin"] 7 | s.homepage = 'http://alexeypetrushin.github.io/vfs' 8 | end -------------------------------------------------------------------------------- /lib/vfs/entries/universal_entry.rb: -------------------------------------------------------------------------------- 1 | module Vfs 2 | class UniversalEntry < Entry 3 | # Attributes. 4 | 5 | def exist? 6 | !!get 7 | end 8 | 9 | def copy_to to, options = {} 10 | from = file? ? to_file : to_dir 11 | from.copy_to to, options 12 | end 13 | 14 | # CRUD. 15 | 16 | def delete 17 | delete_entry 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /spec/misc_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Miscellaneous' do 4 | with_test_dir 5 | 6 | it "File should respond to :to_file, :to_entry" do 7 | path = "#{test_dir.path}/file" 8 | File.open(path, 'w'){|f| f.write 'something'} 9 | 10 | file = nil 11 | begin 12 | file = File.open path 13 | file.to_file.path.should == file.path 14 | file.to_entry.path.should == file.path 15 | ensure 16 | file.close 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'vfs' 2 | 3 | RSpec::Core::ExampleGroup.class_eval do 4 | def self.with_test_dir 5 | before do 6 | @test_dir = "/tmp/test_dir".to_dir 7 | 8 | FileUtils.rm_r test_dir.path if File.exist? test_dir.path 9 | FileUtils.mkdir_p test_dir.path 10 | end 11 | 12 | after do 13 | FileUtils.rm_r test_dir.path if File.exist? test_dir.path 14 | @test_dir = nil 15 | end 16 | end 17 | 18 | def test_dir 19 | @test_dir 20 | end 21 | end -------------------------------------------------------------------------------- /lib/vfs.rb: -------------------------------------------------------------------------------- 1 | module Vfs 2 | autoload :Path, 'vfs/path' 3 | autoload :Error, 'vfs/error' 4 | 5 | autoload :Entry, 'vfs/entries/entry' 6 | autoload :File, 'vfs/entries/file' 7 | autoload :Dir, 'vfs/entries/dir' 8 | autoload :UniversalEntry, 'vfs/entries/universal_entry' 9 | 10 | autoload :EntryProxy, 'vfs/entry_proxy' 11 | 12 | module Drivers 13 | autoload :Local, 'vfs/drivers/local' 14 | end 15 | end 16 | 17 | require 'vfs/vfs' 18 | require 'vfs/integration' -------------------------------------------------------------------------------- /docs/s3_basics.rb: -------------------------------------------------------------------------------- 1 | # Example of [Virtual File System](index.html) working with AWS S3. 2 | # 3 | # This is exactly the same [basic example](basics.html) but this time 4 | # we using S3 as storage instead of local file system. 5 | 6 | # Adding examples folder to load paths. 7 | $LOAD_PATH << File.expand_path("#{__FILE__}/../..") 8 | 9 | # Connecting to S3 and preparing sandbox. You may take a look at 10 | # the [docs/s3_sandbox.rb](s3_sandbox.html) to see the actual code. 11 | require 'docs/s3_sandbox' 12 | 13 | # Now we just executig [basic example](basics.html) 14 | # but with the `$storage` set to AWS S3. 15 | require 'docs/basics' -------------------------------------------------------------------------------- /docs/ssh_basics.rb: -------------------------------------------------------------------------------- 1 | # Example of [Virtual File System](index.html) working with SFTP. 2 | # 3 | # This is exactly the same [basic example](basics.html) but this time 4 | # we using SFTP as storage instead of local file system. 5 | 6 | # Adding examples folder to load paths. 7 | $LOAD_PATH << File.expand_path("#{__FILE__}/../..") 8 | 9 | # Connecting to SFTP and preparing sandbox. You may take a look at 10 | # the [docs/ssh_sandbox.rb](ssh_sandbox.html) to see the actual code. 11 | require 'docs/ssh_sandbox' 12 | 13 | # Now we just executig [basic example](basics.html) 14 | # but with the `$storage` set to SFTP. 15 | require 'docs/basics' -------------------------------------------------------------------------------- /lib/vfs/integration.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def to_entry_on driver = nil 3 | path = self 4 | driver ||= Vfs.default_driver 5 | 6 | path = "./#{path}" unless path =~ /^[\/\.\~]/ 7 | Vfs::Entry.new(driver, path).entry 8 | end 9 | alias_method :to_entry, :to_entry_on 10 | 11 | def to_file_on driver = nil 12 | to_entry_on(driver).file 13 | end 14 | alias_method :to_file, :to_file_on 15 | 16 | def to_dir_on driver = nil 17 | to_entry_on(driver).dir 18 | end 19 | alias_method :to_dir, :to_dir_on 20 | end 21 | 22 | class File 23 | def to_entry 24 | path.to_entry 25 | end 26 | 27 | def to_file 28 | path.to_file 29 | end 30 | end -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | - add drivers: Hadoop DFS, MongoDB, Amazon S3 2 | - remove :host from Vfs to Vos 3 | - efficient version of :copy for files and dirs (there's a problem thought, FileUtils.cp_r overrides files silntly, don't know how to do it)? 4 | - efficient (not copy/delete) versions of move_to, rename 5 | - handy :chmod helpers (via attributes) 6 | - add drivers: remote FS over HTTP? 7 | 8 | # Done: 9 | 10 | - refactor specs with :fakefs (rejected, there are bugs in fakefs) 11 | - glob search for directories: Dir['**/*.yml'] 12 | - Vos: Dir.bash 13 | - File.append 14 | - list of entries/files/dirs 15 | - support for efficient copy for Local and SSH drivers 16 | 17 | # Article 18 | 19 | './result.html'.to_file.write response.body -------------------------------------------------------------------------------- /lib/vfs/vfs.rb: -------------------------------------------------------------------------------- 1 | module Vfs 2 | class << self 3 | def default_driver 4 | ::Vfs::Drivers::Local.new 5 | end 6 | 7 | def to_entry 8 | '/'.to_entry 9 | end 10 | 11 | def to_file 12 | to_entry.file 13 | end 14 | 15 | def to_dir 16 | to_entry.dir 17 | end 18 | 19 | # def [] path 20 | # to_entry[path] 21 | # end 22 | # alias_method :/, :[] 23 | 24 | %w( 25 | entry dir file 26 | entries dirs files 27 | [] / 28 | tmp 29 | ).each do |m| 30 | script = <<-RUBY 31 | def #{m} *a, &b 32 | to_entry.#{m} *a, &b 33 | end 34 | RUBY 35 | eval script, binding, __FILE__, __LINE__ 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /spec/storages/local_spec.rb: -------------------------------------------------------------------------------- 1 | require 'vfs/drivers/local' 2 | require 'vfs/drivers/specification' 3 | 4 | describe Vfs::Drivers::Local do 5 | with_test_dir 6 | 7 | before do 8 | @driver = Vfs::Drivers::Local.new root: test_dir.to_s 9 | @driver.open 10 | end 11 | 12 | after do 13 | @driver.close 14 | end 15 | 16 | it_should_behave_like 'vfs driver basic' 17 | it_should_behave_like 'vfs driver attributes basic' 18 | it_should_behave_like 'vfs driver files' 19 | it_should_behave_like 'vfs driver full attributes for files' 20 | it_should_behave_like 'vfs driver dirs' 21 | it_should_behave_like 'vfs driver full attributes for dirs' 22 | it_should_behave_like 'vfs driver query' 23 | it_should_behave_like 'vfs driver tmp dir' 24 | end -------------------------------------------------------------------------------- /docs/s3_sandbox.rb: -------------------------------------------------------------------------------- 1 | # Example of using AWS S3 as a storage for [Virtual File System](index.html) 2 | 3 | # To use S3 we need the S3 driver, You need 'vos' and 'aws-sdk' gems installed. 4 | # 5 | # gem install vos aws-sdk 6 | # 7 | require 'vfs' 8 | require 'vos' 9 | require 'vos/drivers/s3' 10 | 11 | # Initializing S3 driver, You need to provide Your AWS credentials. 12 | driver = Vos::Drivers::S3.new \ 13 | access_key_id: 'xxx', 14 | secret_access_key: 'xxx', 15 | bucket: 'xxx' 16 | 17 | # After creating driver we can create storage. 18 | box = Vos::Box.new driver 19 | 20 | # Preparing temporary dir (actually, S3 has no dirs, but it can mimic it) 21 | # for sandbox and cleaning it before starting. 22 | $sandbox = box['/tmp/vfs_sandbox'].to_dir.delete -------------------------------------------------------------------------------- /docs/ssh_sandbox.rb: -------------------------------------------------------------------------------- 1 | # Example of using SFTP as a storage for [Virtual File System](vfs) 2 | 3 | # To use SSH/SFTP we need the SSH driver, You need 'vos', 'net-ssh' and 'net-sftp' gems installed. 4 | # 5 | # gem install vos net-ssh net-sftp 6 | # 7 | require 'vfs' 8 | require 'vos' 9 | require 'vos/drivers/ssh' 10 | 11 | # Initializing SSH driver, if You can connect to server using identity file provide host only. 12 | # 13 | # If the connection requires login and password You need to provide it: 14 | # 15 | # driver = Vos::Drivers::Ssh.new \ 16 | # host: 'xxx.com', 17 | # user: 'xxx', 18 | # password: 'xxx' 19 | # 20 | driver = Vos::Drivers::Ssh.new host: 'xxx.com' 21 | 22 | # After creating driver we can create storage. 23 | box = Vos::Box.new driver 24 | 25 | # Preparing temporary dir for sample and cleaning it before starting. 26 | $sandbox = box['/tmp/vfs_sandbox'].to_dir.delete -------------------------------------------------------------------------------- /spec/entry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Entry' do 4 | with_test_dir 5 | 6 | before do 7 | @path = test_dir['a/b/c'] 8 | end 9 | 10 | it "name" do 11 | @path.name.should == 'c' 12 | end 13 | 14 | it "string integration" do 15 | '/'.to_entry.path.should == '/' 16 | 'a'.to_entry.path.should == "./a" 17 | end 18 | 19 | it 'tmp' do 20 | tmp = test_dir.tmp 21 | tmp.should be_dir 22 | tmp.delete 23 | 24 | tmp = nil 25 | test_dir.tmp do |path| 26 | tmp = path 27 | tmp.should be_dir 28 | end 29 | tmp.should_not exist 30 | end 31 | 32 | it 'should respond to local?' do 33 | test_dir.should respond_to(:local?) 34 | end 35 | 36 | it 'created_at, updated_at, size' do 37 | file = test_dir.file('file').write 'data' 38 | file.created_at.class.should == Time 39 | file.updated_at.class.should == Time 40 | file.size.should == 4 41 | end 42 | end -------------------------------------------------------------------------------- /docs/s3_backup.rb: -------------------------------------------------------------------------------- 1 | # Example of creating AWS S3 Backup with [Virtual File System](index.html). 2 | # 3 | # In this example we uploading sample files to S3 and then 4 | # copying it back to local folder. 5 | 6 | # Connecting to S3 and preparing sandbox. You may take a look at 7 | # the [docs/s3_sandbox.rb](s3_sandbox.html) to see the actual code. 8 | $LOAD_PATH << File.expand_path("#{__FILE__}/../..") 9 | require 'docs/s3_sandbox' 10 | s3 = $sandbox 11 | 12 | # Preparing sample files located in our local folder in 13 | # current directory. 14 | current_dir = __FILE__.to_entry.parent 15 | sample_files = current_dir['s3_backup/app'] 16 | 17 | # Uploading sample files to S3. 18 | sample_files.copy_to s3['app'] 19 | p s3['app/files/bos.png'].exist? # => true 20 | 21 | # Preparing local storage for S3 backup. 22 | local_backup = '/tmp/vfs_sandbox/backup'.to_dir.delete 23 | 24 | # Copying files from S3 to local backup directory. 25 | s3['app'].copy_to local_backup['app'] 26 | p local_backup['app/files/bos.png'].exist? # => true -------------------------------------------------------------------------------- /spec/container_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Container' do 4 | with_test_dir 5 | 6 | it "should threat paths as UniversalEntry except it ends with '/'" do 7 | test_dir.should_receive(:entry).with('tmp/a/b') 8 | test_dir['tmp/a/b'] 9 | 10 | test_dir.should_receive(:dir).with('tmp/a/b') 11 | test_dir['tmp/a/b/'] 12 | end 13 | 14 | it '/' do 15 | test_dir[:some_path].should == test_dir / :some_path 16 | test_dir[:some_path][:another_path].should == test_dir / :some_path / :another_path 17 | end 18 | 19 | it "UniversalEntry should be wrapped inside of proxy, Dir and File should not" do 20 | -> {test_dir.dir.proxy?}.should raise_error(NoMethodError) 21 | -> {test_dir.file.proxy?}.should raise_error(NoMethodError) 22 | test_dir.entry.proxy?.should be_true 23 | end 24 | 25 | it "sometimes it also should inexplicitly guess that path is a Dir instead of UniversalEntry (but still wrap it inside of Proxy)" do 26 | 27 | dir = test_dir['tmp/a/..'] 28 | dir.proxy?.should be_true 29 | dir.should be_a(Vfs::Dir) 30 | end 31 | end -------------------------------------------------------------------------------- /lib/vfs/entry_proxy.rb: -------------------------------------------------------------------------------- 1 | # It allows dynamically (magically) switching between UniversalEntry/Dir/File. 2 | module Vfs 3 | class EntryProxy < BasicObject 4 | attr_reader :_target 5 | 6 | def initialize entry 7 | raise 'something wrong happening here!' if entry.respond_to?(:proxy?) and entry.proxy? 8 | self._target = entry 9 | end 10 | 11 | def proxy? 12 | true 13 | end 14 | 15 | protected :==, :equal?, :!, :!= 16 | protected 17 | attr_writer :_target 18 | 19 | def respond_to? m 20 | super or 21 | ::Vfs::UniversalEntry.method_defined?(m) or 22 | ::Vfs::Dir.method_defined?(m) or 23 | ::Vfs::File.method_defined?(m) 24 | end 25 | 26 | def method_missing m, *a, &b 27 | unless _target.respond_to? m 28 | if ::Vfs::UniversalEntry.method_defined? m 29 | self.target = _target.entry 30 | elsif ::Vfs::Dir.method_defined? m 31 | self._target = _target.dir 32 | elsif ::Vfs::File.method_defined? m 33 | self._target = _target.file 34 | end 35 | end 36 | 37 | _target.send m, *a, &b 38 | end 39 | end 40 | end -------------------------------------------------------------------------------- /docs/ssh_deployment.rb: -------------------------------------------------------------------------------- 1 | # Example of Application Deployment using [Virtual File System](index.html). 2 | # 3 | # In this example we uploading sample app files to remote server, 4 | # write database configuration file and restart the server on remote machine. 5 | 6 | # Adding examples folder to load paths. 7 | $LOAD_PATH << File.expand_path("#{__FILE__}/../..") 8 | 9 | # Connecting to SFTP and preparing sandbox. You may take a look at 10 | # the [docs/ssh_sandbox.rb](ssh_sandbox.html) to see the actual code. 11 | require 'docs/ssh_sandbox' 12 | sandbox = $sandbox 13 | 14 | # Preparing sample files located in our local folder in 15 | # current directory. 16 | current_dir = __FILE__.to_entry.parent 17 | sample_app = current_dir['ssh_deployment/app'] 18 | 19 | # Copying application files to remote machine. 20 | app = sandbox['apps/app'] 21 | sample_app.copy_to app 22 | p app['app.rb'].exist? # => true 23 | 24 | # Writing database configuration file. 25 | config = app['config.yml'] 26 | config.write "database: mysql" 27 | config.append "name: app_production" 28 | p app['config.yml'].exist? # => true 29 | 30 | # Updating gems and restarting the server. 31 | p app.bash("echo 'bundle install'") # => bundle install 32 | p app.bash("echo 'server start'") # => server start -------------------------------------------------------------------------------- /spec/universal_entry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'UniversalEntry' do 4 | with_test_dir 5 | 6 | before do 7 | @path = test_dir['a/b/c'] 8 | end 9 | 10 | describe 'existence' do 11 | it "should check both files and dirs" do 12 | @path.should_not exist 13 | @path.dir.create 14 | @path.should be_dir 15 | @path.should exist 16 | 17 | @path.file.create 18 | @path.should be_file 19 | @path.should exist 20 | end 21 | end 22 | 23 | describe 'deleting' do 24 | it "should delete both files and dirs" do 25 | @path.dir.create 26 | @path.should be_dir 27 | @path.entry.delete 28 | @path.should_not exist 29 | 30 | @path.file.create 31 | @path.should be_file 32 | @path.entry.delete 33 | @path.should_not exist 34 | end 35 | 36 | it "shouldn't raise if file not exist" do 37 | @path.delete 38 | end 39 | end 40 | 41 | describe 'copy_to' do 42 | before do 43 | @from = @path.dir 44 | @from.create 45 | @from.file('file').write 'something' 46 | @from.dir('dir').create.tap do |dir| 47 | dir.file('file2').write 'something2' 48 | end 49 | 50 | @to = test_dir['to'] 51 | end 52 | 53 | it "shoud copy dir" do 54 | @from.entry.copy_to @to 55 | @to['dir/file2'].file?.should be_true 56 | end 57 | 58 | it "should copy file" do 59 | @from['file'].entry.copy_to @to 60 | @to.file.should be_true 61 | end 62 | 63 | it "should raise if entry not exist" do 64 | -> {@from['non existing'].entry.copy_to @to}.should raise_error(/not exist/) 65 | end 66 | end 67 | 68 | describe 'move_to' 69 | end -------------------------------------------------------------------------------- /spec/path_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Path" do 4 | before(:all){Path = Vfs::Path} 5 | after(:all){Object.send :remove_const, :Path} 6 | 7 | it 'validations' do 8 | %w( 9 | / 10 | /a 11 | /a/b/c 12 | /a/../c 13 | /a/... 14 | ~/a 15 | ./a 16 | /~a 17 | /a~b 18 | /a.b~ 19 | ).each{|path| Path.should be_valid(path)} 20 | 21 | special = [''] 22 | (%w( 23 | /a/~/c 24 | /a/./c 25 | /a/ 26 | ~/ 27 | ./ 28 | ) + special).each{|path| Path.should_not be_valid(path)} 29 | end 30 | 31 | # it 'tmp' do 32 | # (Path.new('.') + '..').should == './..' 33 | # end 34 | 35 | it 'normalize' do 36 | special = ['/a/../..', nil] 37 | (%w( 38 | /a /a 39 | ~/a ~/a 40 | ./a ./a 41 | /a/../c /c 42 | / / 43 | ~ ~ 44 | /a~/b /a~/b 45 | . . 46 | ) + special).each_slice(2) do |path, normalized_path| 47 | Path.normalize(path).should == normalized_path 48 | end 49 | end 50 | 51 | it "+" do 52 | special = [ 53 | '/a', '../..', nil, 54 | '/', '..', nil, 55 | '.', '..', './..', 56 | ] 57 | (%w( 58 | / /a /a 59 | / ~/a ~/a 60 | / ./a ./a 61 | /a b/c /a/b/c 62 | /a/b/c ../d /a/b/d 63 | ) + special).each_slice(3) do |base, path, sum| 64 | (Path.new(base) + path).should == sum 65 | end 66 | end 67 | 68 | it 'parent' do 69 | special = [ 70 | '/', nil, 71 | '~', nil, 72 | '.', './..' 73 | ] 74 | (%w( 75 | /a/b/c /a/b 76 | ./a/b/c ./a/b 77 | ~/a/b/c ~/a/b 78 | ) + special).each_slice(2) do |path, parent| 79 | Path.new(path).parent.should == parent 80 | end 81 | end 82 | 83 | it "should raise error if current dir outside of root" do 84 | -> {Path.new('/a/../..')}.should raise_error(/outside.*root/) 85 | end 86 | 87 | it "should guess if current dir is a dir" do 88 | [ 89 | '/a', false, 90 | '/', true, 91 | '~', true, 92 | '.', true, 93 | '/a/..', true, 94 | '/a/../b', false, 95 | ].each_slice 2 do |path, result| 96 | Path.new(path).probably_dir?.should == result 97 | end 98 | 99 | path = Path.new('/a/b/c') 100 | [ 101 | path, false, 102 | (path + '..'), true, 103 | path.parent, true, 104 | 105 | (path + '/'), true, 106 | (path + '/a'), false, 107 | ].each_slice 2 do |path, result| 108 | path.probably_dir?.should == result 109 | end 110 | end 111 | 112 | it 'name' do 113 | %w( 114 | /a a 115 | /a/b/c c 116 | / / 117 | ~ ~ 118 | . . 119 | ).each_slice 2 do |path, name| 120 | Path.new(path).name.should == name 121 | end 122 | end 123 | 124 | it 'to_s' do 125 | Path.new.to_s.class.should == String 126 | end 127 | end -------------------------------------------------------------------------------- /lib/vfs/path.rb: -------------------------------------------------------------------------------- 1 | module Vfs 2 | class Path < String 3 | def initialize path = '/', options = {} 4 | if options[:skip_normalization] 5 | super path 6 | @probably_dir = options[:probably_dir] 7 | else 8 | Path.validate! path 9 | path, probably_dir = Path.normalize_to_string path 10 | raise "invalid path '#{path}' (you are outside of the root)!" unless path 11 | super path 12 | @probably_dir = probably_dir 13 | end 14 | end 15 | 16 | def + path = '' 17 | path = path.to_s 18 | Path.validate! path, false 19 | 20 | if Path.absolute?(path) 21 | Path.normalize path 22 | elsif path.empty? 23 | self 24 | else 25 | Path.normalize "#{self}#{'/' unless self == '/'}#{path}" 26 | end 27 | end 28 | 29 | def parent 30 | self + '..' 31 | end 32 | 33 | def probably_dir? 34 | !!@probably_dir 35 | end 36 | 37 | def name 38 | unless @name 39 | root = self[0..0] 40 | @name ||= split('/').last || root 41 | end 42 | @name 43 | end 44 | 45 | class << self 46 | def absolute? path 47 | path =~ /^[\/~\/]|^\.$|^\.\// 48 | end 49 | 50 | def valid? path, forbid_relative = true, &block 51 | result, err = if forbid_relative and !absolute?(path) 52 | [false, "path must be started with '/', or '.'"] 53 | elsif path =~ /.+\/~$|.+\/$|\/\.$/ 54 | [false, "path can't be ended with '/', '/~', or '/.'"] 55 | elsif path =~ /\/\/|\/~\/|\/\.\// 56 | [false, "path can't include '/./', '/~/', '//' combinations!"] 57 | # elsif path =~ /.+[~]|\/\.\// 58 | # [false, "'~', or '.' can be present only at the begining of string"] 59 | else 60 | [true, nil] 61 | end 62 | 63 | block.call err if block and !result and err 64 | result 65 | end 66 | 67 | def normalize path 68 | path, probably_dir = normalize_to_string path 69 | unless path 70 | nil 71 | else 72 | Path.new(path, skip_normalization: true, probably_dir: probably_dir) 73 | end 74 | end 75 | 76 | def validate! path, forbid_relative = true 77 | valid?(path, forbid_relative){|error| raise "invalid path '#{path}' (#{error})!"} 78 | end 79 | 80 | def normalize_to_string path 81 | root = path[0..0] 82 | result, probably_dir = [], false 83 | 84 | parts = path.split('/')[1..-1] 85 | if parts 86 | parts.each do |part| 87 | if part == '..' and (root != '.' or (root == '.' and result.size > 0)) 88 | return nil, false unless result.size > 0 89 | result.pop 90 | probably_dir ||= true 91 | else 92 | result << part 93 | probably_dir &&= false 94 | end 95 | end 96 | end 97 | normalized_path = result.join('/') 98 | 99 | probably_dir ||= true if normalized_path.empty? 100 | 101 | return "#{root}#{'/' unless root == '/' or normalized_path.empty?}#{normalized_path}", probably_dir 102 | end 103 | end 104 | 105 | # protected 106 | # def delete_dir_mark 107 | # path = path.to_s.sub(%r{/$}, '') 108 | # end 109 | # 110 | # 111 | # def root_path? path 112 | # path =~ /^[#{ROOT_SYMBOLS}]$/ 113 | # end 114 | # 115 | # def split_path path 116 | # path.split(/#{ROOT_SYMBOLS}/) 117 | # end 118 | # 119 | # def dir_mark? path 120 | # path =~ %r{/$} 121 | # end 122 | end 123 | end -------------------------------------------------------------------------------- /lib/vfs/entries/entry.rb: -------------------------------------------------------------------------------- 1 | module Vfs 2 | class Entry 3 | attr_reader :driver, :path, :path_cache 4 | 5 | def initialize *args 6 | if args.size == 1 and args.first.is_a? Entry 7 | entry = args.first 8 | @path_cache = entry.path_cache 9 | @driver, @path = entry.driver, entry.path 10 | else 11 | driver, path = *args 12 | @path_cache = Path.new path 13 | @driver, @path = driver, path_cache.to_s 14 | end 15 | raise "driver not defined!" unless self.driver 16 | end 17 | 18 | # Navigation. 19 | def parent 20 | Dir.new(driver, path_cache.parent) 21 | end 22 | 23 | # Transformations. 24 | 25 | def dir path = nil 26 | if path 27 | new_path = path_cache + path 28 | Dir.new driver, new_path 29 | else 30 | Dir.new self 31 | end 32 | end 33 | alias_method :to_dir, :dir 34 | 35 | def file path = nil 36 | if path 37 | new_path = path_cache + path 38 | File.new driver, new_path 39 | else 40 | File.new self 41 | end 42 | end 43 | alias_method :to_file, :file 44 | 45 | def entry path = nil 46 | entry = if path 47 | new_path = path_cache + path 48 | klass = new_path.probably_dir? ? Dir : UniversalEntry 49 | klass.new driver, new_path 50 | else 51 | UniversalEntry.new self 52 | end 53 | EntryProxy.new entry 54 | end 55 | alias_method :to_entry, :entry 56 | 57 | # Attributes. 58 | 59 | def get attr_name = nil 60 | attrs = driver.open{driver.attributes(path)} 61 | (attr_name and attrs) ? attrs[attr_name] : attrs 62 | end 63 | 64 | def set options 65 | # TODO2 set attributes 66 | not_implemented 67 | end 68 | 69 | def dir?; !!get(:dir) end 70 | def file?; !!get(:file) end 71 | def created_at; get :created_at end 72 | def updated_at; get :updated_at end 73 | 74 | # Miscellaneous. 75 | 76 | def name 77 | path_cache.name 78 | end 79 | 80 | def tmp &block 81 | driver.open do 82 | if block 83 | driver.tmp do |path| 84 | block.call Dir.new(driver, path) 85 | end 86 | else 87 | Dir.new driver, driver.tmp 88 | end 89 | end 90 | end 91 | 92 | def local? 93 | driver.local? 94 | end 95 | 96 | # Utils. 97 | 98 | def inspect 99 | "#{driver}#{':' unless driver.to_s.empty?}#{path}" 100 | end 101 | alias_method :to_s, :inspect 102 | 103 | def == other 104 | return false unless other.is_a? Entry 105 | driver == other.driver and path == other.path 106 | end 107 | 108 | def hash 109 | driver.hash + path.hash 110 | end 111 | 112 | def eql? other 113 | return false unless other.class == self.class 114 | driver.eql?(other.driver) and path.eql?(other.path) 115 | end 116 | 117 | def destroy *args; delete *args end 118 | def remove *args; delete *args end 119 | 120 | protected 121 | def delete_entry first = :file, second = :dir 122 | driver.open do 123 | begin 124 | driver.send :"delete_#{first}", path 125 | rescue StandardError => e 126 | attrs = get 127 | if attrs and attrs[first] 128 | # some unknown error 129 | raise e 130 | elsif attrs and attrs[second] 131 | driver.send :"delete_#{second}", path 132 | else 133 | # Do nothing, entry already not exist. 134 | end 135 | end 136 | end 137 | self 138 | end 139 | end 140 | end -------------------------------------------------------------------------------- /lib/vfs/entries/file.rb: -------------------------------------------------------------------------------- 1 | module Vfs 2 | class File < Entry 3 | # Attributes. 4 | alias_method :exist?, :file? 5 | 6 | # CRUD. 7 | 8 | def read options = {}, &block 9 | options[:bang] = true unless options.include? :bang 10 | driver.open do 11 | begin 12 | if block 13 | driver.read_file path, &block 14 | else 15 | data = "" 16 | driver.read_file(path){|buff| data << buff} 17 | data 18 | end 19 | rescue StandardError => e 20 | raise Vfs::Error, "can't read Dir #{self}!" if dir.exist? 21 | attrs = get 22 | if attrs and attrs[:file] 23 | # unknown internal error 24 | raise e 25 | elsif attrs and attrs[:dir] 26 | raise Error, "You are trying to read Dir '#{self}' as if it's a File!" 27 | else 28 | if options[:bang] 29 | raise Error, "file #{self} not exist!" 30 | else 31 | block ? block.call('') : '' 32 | end 33 | end 34 | end 35 | end 36 | end 37 | 38 | # def content options = {} 39 | # read options 40 | # end 41 | 42 | def create options = {} 43 | write '', options 44 | self 45 | end 46 | 47 | def write *args, &block 48 | if block 49 | options = args.first || {} 50 | else 51 | data, options = *args 52 | data ||= "" 53 | options ||= {} 54 | end 55 | raise "can't do :override and :append at the same time!" if options[:override] and options[:append] 56 | 57 | driver.open do 58 | try = 0 59 | begin 60 | try += 1 61 | if block 62 | driver.write_file(path, options[:append], &block) 63 | else 64 | driver.write_file(path, options[:append]){|writer| writer.write data} 65 | end 66 | rescue StandardError => error 67 | parent = self.parent 68 | if entry.exist? 69 | entry.delete 70 | elsif !parent.exist? 71 | parent.create(options) 72 | else 73 | # unknown error 74 | raise error 75 | end 76 | 77 | try < 2 ? retry : raise(error) 78 | end 79 | end 80 | self 81 | end 82 | 83 | def append *args, &block 84 | options = (args.last.is_a?(Hash) && args.pop) || {} 85 | options[:append] = true 86 | write(*(args << options), &block) 87 | end 88 | 89 | def update options = {}, &block 90 | data = read options 91 | write block.call(data), options 92 | end 93 | 94 | def delete 95 | delete_entry 96 | end 97 | 98 | # Transfers. 99 | 100 | def copy_to to, options = {} 101 | raise Error, "you can't copy to itself" if self == to 102 | 103 | target = if to.is_a? File 104 | to 105 | elsif to.is_a? Dir 106 | to.file #(name) 107 | elsif to.is_a? UniversalEntry 108 | to.file 109 | else 110 | raise "can't copy to unknown Entry!" 111 | end 112 | 113 | target.write options do |writer| 114 | read(options){|buff| writer.write buff} 115 | end 116 | 117 | target 118 | end 119 | 120 | def move_to to 121 | copy_to to 122 | delete 123 | to 124 | end 125 | 126 | # Extra Stuff. 127 | 128 | def render *args 129 | require 'tilt' 130 | 131 | args.unshift Object.new if args.size == 1 and args.first.is_a?(Hash) 132 | 133 | template = Tilt.new(path){read} 134 | template.render *args 135 | end 136 | 137 | def size; get :size end 138 | 139 | def basename 140 | ::File.basename(name, ::File.extname(name)) 141 | end 142 | 143 | def extension 144 | ::File.extname(name).sub(/^\./, '') 145 | end 146 | end 147 | end -------------------------------------------------------------------------------- /lib/vfs/drivers/local.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'tempfile' 3 | 4 | module Vfs 5 | module Drivers 6 | class Local 7 | class Writer 8 | def initialize out; @out = out end 9 | 10 | def write data 11 | @out.write data 12 | end 13 | end 14 | 15 | DEFAULT_BUFFER = 1000 * 1024 16 | 17 | def initialize options = {} 18 | options = options.clone 19 | @root = options.delete(:root) || '' 20 | raise "invalid options #{options}" unless options.empty? 21 | end 22 | 23 | def open &block 24 | block.call self if block 25 | end 26 | def close; end 27 | 28 | attr_writer :buffer 29 | def buffer 30 | @buffer || DEFAULT_BUFFER 31 | end 32 | 33 | # Attributes. 34 | 35 | def attributes path 36 | path = with_root path 37 | 38 | stat = ::File.stat path 39 | attrs = {} 40 | attrs[:file] = !!stat.file? 41 | attrs[:dir] = !!stat.directory? 42 | 43 | # attributes special for file system 44 | attrs[:created_at] = stat.ctime 45 | attrs[:updated_at] = stat.mtime 46 | attrs[:size] = stat.size if attrs[:file] 47 | attrs 48 | rescue Errno::ENOTDIR 49 | nil 50 | rescue Errno::ENOENT 51 | nil 52 | end 53 | 54 | def set_attributes path, attrs 55 | # TODO2 set attributes. 56 | not_implemented 57 | end 58 | 59 | # File. 60 | 61 | def read_file path, &block 62 | path = with_root path 63 | ::File.open path, 'r' do |is| 64 | while buff = is.gets(self.buffer || DEFAULT_BUFFER) 65 | block.call buff 66 | end 67 | end 68 | end 69 | 70 | def write_file original_path, append, &block 71 | path = with_root original_path 72 | 73 | option = append ? 'a' : 'w' 74 | ::File.open path, "#{option}b" do |out| 75 | block.call Writer.new(out) 76 | end 77 | end 78 | 79 | def delete_file path 80 | path = with_root path 81 | ::File.delete path 82 | end 83 | 84 | # def move_file from, to 85 | # FileUtils.mv from, to 86 | # end 87 | 88 | # Dir. 89 | def create_dir path 90 | path = with_root path 91 | ::Dir.mkdir path 92 | end 93 | 94 | def delete_dir original_path 95 | path = with_root original_path 96 | ::FileUtils.rm_r path 97 | end 98 | 99 | def each_entry path, query, &block 100 | path = with_root path 101 | 102 | if query 103 | path_with_trailing_slash = path == '/' ? path : "#{path}/" 104 | ::Dir["#{path_with_trailing_slash}#{query}"].each do |absolute_path| 105 | name = absolute_path.sub path_with_trailing_slash, '' 106 | block.call name, ->{::File.directory?(absolute_path) ? :dir : :file} 107 | end 108 | else 109 | ::Dir.foreach path do |name| 110 | next if name == '.' or name == '..' 111 | block.call name, ->{::File.directory?("#{path}/#{name}") ? :dir : :file} 112 | end 113 | end 114 | end 115 | 116 | # def efficient_dir_copy from, to, override 117 | # return false if override # FileUtils.cp_r doesn't support this behaviour 118 | # 119 | # from.driver.open_fs do |from_fs| 120 | # to.driver.open_fs do |to_fs| 121 | # if from_fs.local? and to_fs.local? 122 | # FileUtils.cp_r from.path, to.path 123 | # true 124 | # else 125 | # false 126 | # end 127 | # end 128 | # end 129 | # end 130 | 131 | # Other. 132 | 133 | def local?; true end 134 | 135 | def tmp &block 136 | path = "/tmp/#{rand(10**6)}" 137 | if block 138 | begin 139 | ::FileUtils.mkdir_p with_root(path) 140 | block.call path 141 | ensure 142 | ::FileUtils.rm_r with_root(path) if ::File.exist? with_root(path) 143 | end 144 | else 145 | ::FileUtils.mkdir_p with_root(path) 146 | path 147 | end 148 | end 149 | 150 | def to_s; '' end 151 | 152 | protected 153 | def root 154 | @root || raise('root not defined!') 155 | end 156 | 157 | def with_root path 158 | path == '/' ? root : root + path 159 | end 160 | end 161 | end 162 | end -------------------------------------------------------------------------------- /docs/site/s3_basics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | s3_basics.rb 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 59 | 60 | 132 |
133 | 134 | 135 | -------------------------------------------------------------------------------- /docs/site/ssh_basics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ssh_basics.rb 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 59 | 60 | 132 |
133 | 134 | 135 | -------------------------------------------------------------------------------- /docs/basics.rb: -------------------------------------------------------------------------------- 1 | # Virtual File System provides **clean, simple and unified API over different storages** 2 | # (Local File System, AWS S3, SFTP, ...). 3 | # 4 | # - very simple and intuitive API. 5 | # - same API for different storages. 6 | # - work simultaneously with multiple storages. 7 | # - small codebase, easy to learn and extend. 8 | # - driver implementation is very simple, it is easy to create new drivers. 9 | # 10 | # Such unified API is possible because although the API of storages are different the core 11 | # concept are almost the same. 12 | # 13 | # Install Vfs with Rubygems: 14 | # 15 | # gem install vfs 16 | # 17 | # Once installed, You can proceed with the example below. It uses local file system as storage, 18 | # there's also [S3 version](s3_basics.html) and [SFTP version](ssh_basics.html) 19 | # (also [S3 backup](s3_backup.html) and [SSH/SFTP deployment](ssh_deployment.html) examples 20 | # availiable). 21 | # 22 | # The project is [hosted on GitHub](https://github.com/alexeypetrushin/vfs). 23 | # You can report bugs and discuss features on the 24 | # [issues page](https://github.com/alexeypetrushin/vfs/issues). 25 | 26 | # ### Example 27 | 28 | # Preparing sandbox for our sample and cleaning it before starting 29 | # (ignore the `$sandbox` variable, it's needed to reuse this code in S3 and SSH samples). 30 | require 'vfs' 31 | sandbox = $sandbox || '/tmp/vfs_sandbox'.to_dir.delete 32 | 33 | # Creating simple Hello World project. 34 | project = sandbox['hello_world'] 35 | 36 | # Writing readme file (note that parent dirs where created automatically). 37 | project['readme.txt'].write 'My App' 38 | 39 | # We can assign files and dirs to variables, now the `readme` variable refers to our readme.txt file. 40 | readme = project['readme.txt'] 41 | 42 | # Let's ensure that it's all ok with our readme file and check its attributes. 43 | p readme.name # => readme.txt 44 | p [readme.basename, readme.extension] # => ['readme', 'txt'] 45 | p readme.path # => /.../readme.txt 46 | p readme.exist? # => true 47 | p readme.file? # => true 48 | p readme.dir? # => false 49 | p readme.size # => 6 50 | p readme.created_at # => 2011-09-09 13:20:43 +0400 51 | p readme.updated_at # => 2011-09-09 13:20:43 +0400 52 | 53 | # Reading - You can read all at once or do it sequentially (input stream 54 | # will be automatically splitted into chunks of reasonable size). 55 | p readme.read # => "My shiny App" 56 | readme.read{|chunk| p chunk} # => "My shiny App" 57 | 58 | # The same for writing - write all at once or do it sequentially 59 | # (if there's no file it will be created, if it exists it will be rewriten). 60 | readme.write "My App v2" 61 | readme.write{|stream| stream.write "My App v3"} 62 | p readme.read # => "My shiny App v3" 63 | 64 | # Appending content to existing file. 65 | readme.append "How to install ..." 66 | p readme.size # => 27 67 | 68 | # Copying and Moving. It also works exactly the same 69 | # way if You copy or move files and dirs to other storages. 70 | readme.copy_to project['docs/readme.txt'] 71 | p project['docs/readme.txt'].exist? # => true 72 | p readme.exist? # => true 73 | 74 | readme.move_to project['docs/readme.txt'] 75 | p project['docs/readme.txt'].exist? # => true 76 | p readme.exist? # => false 77 | 78 | # Let's add empty Rakefile to our project. 79 | project['Rakefile'].write 80 | 81 | # Operations with directories - checking our project exists and not empty. 82 | p project.exist? # => true 83 | p project.empty? # => false 84 | 85 | # Listing dir content. There are two versions of methods - 86 | # without block the result will be Array of Entries, with block 87 | # it will iterate over directory sequentially. 88 | p project.entries # => [/.../docs, /.../Rakefile] 89 | p project.files # => [/.../Rakefile] 90 | p project.dirs # => [/.../docs] 91 | project.entries do |entry| # => ["docs", false] 92 | p [entry.name, entry.file?] # => ["Rakefile", true] 93 | end 94 | p project.include?('Rakefile') # => true 95 | 96 | # You can also use glob (if storage support it). 97 | if project.driver.local? 98 | p project.entries('**/Rake*') # => [/.../Rakefile] 99 | p project['**/Rake*'] # => [/.../Rakefile] 100 | 101 | # The result of dir listing is just an array of Entries, so 102 | # You can use it to do interesting things. For example this code will 103 | # calculates the size of sources in our project. 104 | project['**/*.rb'].collect(&:size).reduce(0, :+) 105 | end 106 | 107 | # Copying and moving - let's create another project by cloning our hello_world. 108 | project.copy_to sandbox['another_project'] 109 | p sandbox['another_project'].entries # => [/.../docs, .../Rakefile] 110 | 111 | # Cleaning sandbox. 112 | sandbox.delete 113 | 114 | # In this example we covering basics of **Virtual File System**, if You are interesting You can also take 115 | # a look at [S3 backup](s3_backup.html) and [SSH/SFTP deployment](ssh_deployment.html) examples. -------------------------------------------------------------------------------- /lib/vfs/drivers/specification.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'vfs driver basic' do 2 | it 'should respond to :local?' do 3 | @driver.should respond_to(:local?) 4 | end 5 | 6 | it "should provide open method" do 7 | @driver.open 8 | @driver.open{'result'}.should == 'result' 9 | end 10 | end 11 | 12 | shared_examples_for 'vfs driver attributes basic' do 13 | it 'should have root dir' do 14 | attrs = @driver.attributes('/') 15 | attrs[:dir].should be_true 16 | attrs[:file].should be_false 17 | end 18 | 19 | it "attributes should return nil if there's no entry" do 20 | @driver.attributes('/non_existing_entry').should be_nil 21 | end 22 | end 23 | 24 | shared_examples_for 'vfs driver files' do 25 | it "file attributes" do 26 | @driver.attributes('/file').should be_nil 27 | 28 | @driver.write_file('/file', false){|w| w.write 'something'} 29 | attrs = @driver.attributes('/file') 30 | attrs[:file].should be_true 31 | attrs[:dir].should be_false 32 | end 33 | 34 | it "read, write, append" do 35 | # Write. 36 | @driver.write_file('/file', false){|w| w.write 'something'} 37 | @driver.attributes('/file')[:file].should == true 38 | 39 | # Read. 40 | data = "" 41 | @driver.read_file('/file'){|buff| data << buff} 42 | data.should == 'something' 43 | 44 | # Append. 45 | @driver.write_file('/file', true){|w| w.write ' another'} 46 | data = "" 47 | @driver.read_file('/file'){|buff| data << buff} 48 | data.should == 'something another' 49 | end 50 | 51 | it "delete_file" do 52 | @driver.write_file('/file', false){|w| w.write 'something'} 53 | @driver.attributes('/file')[:file].should be_true 54 | @driver.delete_file('/file') 55 | @driver.attributes('/file').should be_nil 56 | end 57 | end 58 | 59 | shared_examples_for 'vfs driver full attributes for files' do 60 | it "attributes for files" do 61 | @driver.write_file('/file', false){|w| w.write 'something'} 62 | attrs = @driver.attributes('/file') 63 | attrs[:file].should be_true 64 | attrs[:dir].should be_false 65 | attrs[:created_at].class.should == Time 66 | attrs[:updated_at].class.should == Time 67 | attrs[:size].should == 9 68 | end 69 | end 70 | 71 | shared_examples_for 'vfs driver dirs' do 72 | it "directory crud" do 73 | @driver.attributes('/dir').should be_nil 74 | 75 | @driver.create_dir('/dir') 76 | attrs = @driver.attributes('/dir') 77 | attrs[:file].should be_false 78 | attrs[:dir].should be_true 79 | 80 | @driver.delete_dir('/dir') 81 | @driver.attributes('/dir').should be_nil 82 | end 83 | 84 | it 'should delete not-empty directories' do 85 | @driver.create_dir('/dir') 86 | @driver.create_dir('/dir/dir2') 87 | @driver.write_file('/dir/dir2/file', false){|w| w.write 'something'} 88 | @driver.attributes('/dir').should_not be_nil 89 | 90 | @driver.delete_dir('/dir') 91 | @driver.attributes('/dir').should be_nil 92 | end 93 | 94 | it 'each' do 95 | -> {@driver.each_entry('/not_existing_dir', nil){|path, type| list[path] = type}}.should raise_error 96 | 97 | @driver.create_dir '/dir' 98 | @driver.create_dir('/dir/dir2') 99 | @driver.write_file('/dir/file', false){|w| w.write 'something'} 100 | 101 | list = {} 102 | @driver.each_entry '/dir', nil do |path, type| 103 | type = type.call if type.is_a? Proc 104 | list[path] = type 105 | end 106 | 107 | list.should == {'dir2' => :dir, 'file' => :file} 108 | end 109 | 110 | # it "upload_directory & download_directory" do 111 | # upload_path_check = "#{@remote_path}/dir2/file" 112 | # check_attributes upload_path_check, nil 113 | # @driver.upload_directory(@from_local, @remote_path) 114 | # check_attributes upload_path_check, file: true, dir: false 115 | # 116 | # download_path_check = "#{@to_local}/dir2/file" 117 | # File.exist?(download_path_check).should be_false 118 | # @driver.download_directory(@remote_path, @to_local) 119 | # File.exist?(download_path_check).should be_true 120 | # end 121 | end 122 | 123 | shared_examples_for 'vfs driver query' do 124 | it 'each with query' do 125 | @driver.create_dir '/dir' 126 | @driver.create_dir('/dir/dir_a') 127 | @driver.create_dir('/dir/dir_b') 128 | @driver.write_file('/dir/file_a', false){|w| w.write 'something'} 129 | 130 | list = {} 131 | @driver.each_entry '/dir', '*_a' do |path, type| 132 | type = type.call if type.is_a? Proc 133 | list[path] = type 134 | end 135 | 136 | list.should == {'dir_a' => :dir, 'file_a' => :file} 137 | end 138 | end 139 | 140 | shared_examples_for 'vfs driver full attributes for dirs' do 141 | it "attributes for dirs" do 142 | @driver.create_dir('/dir') 143 | attrs = @driver.attributes('/dir') 144 | attrs[:file].should be_false 145 | attrs[:dir].should be_true 146 | attrs[:created_at].class.should == Time 147 | attrs[:updated_at].class.should == Time 148 | attrs.should_not include(:size) 149 | end 150 | end 151 | 152 | shared_examples_for 'vfs driver tmp dir' do 153 | it "tmp dir" do 154 | dir = @driver.tmp 155 | @driver.attributes(dir).should_not be_nil 156 | @driver.delete_dir dir 157 | @driver.attributes(dir).should be_nil 158 | 159 | dir = nil 160 | @driver.tmp do |tmp_dir| 161 | dir = tmp_dir 162 | @driver.attributes(dir).should_not be_nil 163 | end 164 | @driver.attributes(dir).should be_nil 165 | end 166 | end -------------------------------------------------------------------------------- /docs/site/s3_sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | s3_sandbox.rb 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 59 | 60 | 150 |
151 | 152 | 153 | -------------------------------------------------------------------------------- /docs/site/ssh_sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ssh_sandbox.rb 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 59 | 60 | 151 |
152 | 153 | 154 | -------------------------------------------------------------------------------- /docs/site/s3_backup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | s3_backup.rb 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 59 | 60 | 167 |
168 | 169 | 170 | -------------------------------------------------------------------------------- /spec/file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'File' do 4 | with_test_dir 5 | 6 | before do 7 | @path = test_dir['a/b/c'] 8 | end 9 | 10 | describe 'existence' do 11 | it "should check only files" do 12 | @path.should_not exist 13 | @path.dir.create 14 | @path.should be_dir 15 | @path.file.should_not exist 16 | @path.file.create 17 | @path.should be_file 18 | @path.file.should exist 19 | end 20 | end 21 | 22 | describe 'read' do 23 | it 'should raise error if not exist' do 24 | -> {@path.read}.should raise_error(Vfs::Error, /not exist/) 25 | -> {@path.read{|buff|}}.should raise_error(Vfs::Error, /not exist/) 26 | end 27 | 28 | it 'should not raise error in silent mode' do 29 | @path.read(bang: false).should == '' 30 | data = ""; @path.read(bang: false){|buff| data << buff}; data.should == '' 31 | end 32 | 33 | it "reading" do 34 | @path.write('something') 35 | 36 | @path.read.should == 'something' 37 | @path.read(bang: false).should == 'something' 38 | data = ""; @path.read{|buff| data << buff}; data.should == 'something' 39 | end 40 | 41 | # it 'content' do 42 | # @path.write('something') 43 | # @path.content.should == 'something' 44 | # end 45 | end 46 | 47 | describe 'creation' do 48 | it 'create' do 49 | file = @path.file 50 | 51 | file.should_receive(:write).with('', {}) 52 | file.create 53 | end 54 | 55 | it 'should be chainable' do 56 | @path.file.create.should == @path 57 | end 58 | end 59 | 60 | describe 'write' do 61 | it 'should create parent dirs if not exists' do 62 | @path.parent.should_not exist 63 | @path.write 'something' 64 | @path.read.should == 'something' 65 | end 66 | 67 | it 'should write empty file' do 68 | @path.write 69 | @path.read.should == '' 70 | end 71 | 72 | it 'should override existing file' do 73 | @path.write 'something' 74 | @path.should be_file 75 | @path.write 'other' 76 | @path.read.should == 'other' 77 | end 78 | 79 | it 'should override existing dir' do 80 | @path.dir.create 81 | @path.should be_dir 82 | @path.write 'another' 83 | @path.read.should == 'another' 84 | end 85 | 86 | it 'writing' do 87 | @path.write 'something' 88 | @path.read.should == 'something' 89 | 90 | @path.write do |writer| 91 | writer.write 'another' 92 | end 93 | @path.read.should == 'another' 94 | end 95 | 96 | it 'append' do 97 | file = @path.file 98 | file.should_receive(:write).with('something', append: true) 99 | file.append 'something' 100 | end 101 | 102 | it 'should correctly display errors (from error)' do 103 | -> {test_dir['test'].write{|writer| raise 'some error'}}.should raise_error(/some error/) 104 | end 105 | end 106 | 107 | it 'update' do 108 | @path.write 'something' 109 | @path.update do |data| 110 | data.should == 'something' 111 | 'another' 112 | end 113 | @path.read.should == 'another' 114 | end 115 | 116 | describe 'copying' do 117 | before do 118 | @from = @path.file 119 | @from.write('something') 120 | end 121 | 122 | it 'should not copy to itself' do 123 | -> {@from.copy_to @from}.should raise_error(Vfs::Error, /itself/) 124 | end 125 | 126 | def check_copy_for to 127 | target = @from.copy_to to 128 | target.read.should == 'something' 129 | target.should == to 130 | 131 | # overriding 132 | @from.write 'another' 133 | target = @from.copy_to to 134 | target.read.should == 'another' 135 | target.should == to 136 | end 137 | 138 | it 'should copy to file (and overwrite)' do 139 | check_copy_for test_dir.file('to') 140 | end 141 | 142 | it 'should copy to dir (and overwrite)' do 143 | check_copy_for test_dir.dir("to") 144 | end 145 | 146 | it 'should copy to UniversalEntry (and overwrite)' do 147 | check_copy_for test_dir.entry('to') 148 | end 149 | 150 | it 'should be chainable' do 151 | to = test_dir['to'] 152 | @from.copy_to(to).should == to 153 | end 154 | 155 | it "should autocreate parent's path if not exist (from error)" do 156 | to = test_dir['parent_path/to'] 157 | @from.copy_to(to) 158 | to.read.should == 'something' 159 | end 160 | end 161 | 162 | describe 'moving' do 163 | it 'move_to' do 164 | from, to = @path.file('from'), @path.file('to') 165 | from.should_receive(:copy_to).with(to) 166 | from.should_receive(:delete).with() 167 | from.move_to to 168 | end 169 | 170 | it 'should be chainable' do 171 | from, to = @path.file('from').create, @path.file('to') 172 | from.move_to(to).should == to 173 | end 174 | end 175 | 176 | describe 'deleting' do 177 | it "should not raise error if it's trying to delete a dir" do 178 | @path.dir.create 179 | @path.file.delete 180 | @path.entry.should_not exist 181 | end 182 | 183 | it "shouldn't raise if file not exist" do 184 | @path.file.delete 185 | end 186 | 187 | it 'should be chainable' do 188 | @path.file.delete.should == @path 189 | end 190 | end 191 | 192 | describe "extra stuff" do 193 | # Executing specs for templates only if tilt is available. 194 | begin require 'tilt'; rescue; end 195 | if Object.const_defined? :Tilt 196 | it 'render' do 197 | template = test_dir / 'letter.erb' 198 | template.write "Hello dear <%= name %>" 199 | template.render(name: 'Mary').should == "Hello dear Mary" 200 | end 201 | else 202 | warn 'Tilt template engine not available, skipping specs.' 203 | end 204 | 205 | begin 206 | require 'haml' 207 | 208 | it 'render using other template engines' do 209 | template = test_dir / 'letter.haml' 210 | template.write "Hello dear \#{name}" 211 | template.render(name: 'Mary').should =~ /Hello dear Mary/ 212 | end 213 | rescue LoadError 214 | warn "no :haml template engine, skipping rendering with haml specs" 215 | end 216 | 217 | it 'size' do 218 | @path.file.write('data').size.should == 4 219 | end 220 | end 221 | end -------------------------------------------------------------------------------- /old/hash_fs.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Very dirty and inefficient In Memory File System, mainly for tests. 3 | # 4 | module Vfs 5 | module Drivers 6 | class HashFs < Hash 7 | def initialize 8 | super 9 | self['/'] = {dir: true} 10 | end 11 | 12 | 13 | def open &block 14 | block.call self 15 | end 16 | 17 | # 18 | # Attributes 19 | # 20 | def attributes path 21 | base, name = split_path path 22 | 23 | # if path == '/' 24 | # return {dir: true, file: false} 25 | # end 26 | # 27 | stat = cd(base)[name] 28 | attrs = {} 29 | attrs[:file] = !!stat[:file] 30 | attrs[:dir] = !!stat[:dir] 31 | attrs 32 | rescue Exception 33 | {} 34 | end 35 | 36 | def set_attributes path, attrs 37 | raise 'not supported' 38 | end 39 | 40 | 41 | # 42 | # File 43 | # 44 | def read_file path, &block 45 | base, name = split_path path 46 | assert cd(base)[name], :include?, :file 47 | block.call cd(base)[name][:content] 48 | end 49 | 50 | def write_file path, append, &block 51 | base, name = split_path path 52 | 53 | os = if append 54 | file = cd(base)[name] 55 | file ? file[:content] : '' 56 | else 57 | assert_not cd(base), :include?, name 58 | '' 59 | end 60 | writer = -> buff {os << buff} 61 | block.call writer 62 | 63 | cd(base)[name] = {file: true, content: os} 64 | end 65 | 66 | def delete_file path 67 | base, name = split_path path 68 | assert cd(base)[name], :include?, :file 69 | cd(base).delete name 70 | end 71 | 72 | # def move_file path 73 | # raise 'not supported' 74 | # end 75 | 76 | 77 | # 78 | # Dir 79 | # 80 | def create_dir path 81 | base, name = split_path path 82 | assert_not cd(base), :include?, name 83 | cd(base)[name] = {dir: true} 84 | end 85 | 86 | def delete_dir path 87 | base, name = split_path path 88 | assert cd(base)[name], :include?, :dir 89 | # empty = true 90 | # cd(base)[name].each do |key, value| 91 | # empty = false if key.is_a? String 92 | # end 93 | # raise 'you are trying to delete not empty dir!' unless empty 94 | cd(base).delete name 95 | end 96 | 97 | # def move_dir path 98 | # raise 'not supported' 99 | # end 100 | 101 | def each_entry path, query, &block 102 | raise "hash_fs not support :each_entry with query!" if query 103 | 104 | base, name = split_path path 105 | assert cd(base)[name], :include?, :dir 106 | cd(base)[name].each do |relative_name, content| 107 | next if relative_name.is_a? Symbol 108 | if content[:dir] 109 | block.call relative_name, :dir 110 | else 111 | block.call relative_name, :file 112 | end 113 | end 114 | end 115 | 116 | def efficient_dir_copy from, to, override 117 | from.driver.open do |from_fs| 118 | to.driver.open do |to_fs| 119 | if from_fs == to_fs 120 | for_spec_helper_effective_copy_used 121 | 122 | _efficient_dir_copy from.path, to.path, override 123 | true 124 | else 125 | false 126 | end 127 | end 128 | end 129 | end 130 | 131 | def _efficient_dir_copy from_path, to_path, override 132 | from_base, from_name = split_path from_path 133 | assert cd(from_base)[from_name], :include?, :dir 134 | 135 | to_base, to_name = split_path to_path 136 | # assert_not cd(to_base), :include?, to_name 137 | 138 | if cd(to_base).include? to_name 139 | if cd(to_base)[to_name][:dir] 140 | each_entry from_path, nil do |name, type| 141 | if type == :dir 142 | _efficient_dir_copy "#{from_path}/#{name}", "#{to_path}/#{name}", override 143 | else 144 | raise "file #{to_path}/#{name} already exist!" if cd(to_base)[to_name].include?(name) and !override 145 | cd(to_base)[to_name][name] = cd(from_base)[from_name][name].clone 146 | end 147 | end 148 | else 149 | raise "can't copy dir #{from_path} to file #{to_path}!" 150 | end 151 | else 152 | cd(to_base)[to_name] = {dir: true} 153 | _efficient_dir_copy from_path, to_path, override 154 | end 155 | end 156 | protected :_efficient_dir_copy 157 | def for_spec_helper_effective_copy_used; end 158 | 159 | # def upload_directory from_local_path, to_remote_path 160 | # FileUtils.cp_r from_local_path, to_remote_path 161 | # end 162 | # 163 | # def download_directory from_remote_path, to_local_path 164 | # FileUtils.cp_r from_remote_path, to_local_path 165 | # end 166 | 167 | 168 | # 169 | # Other 170 | # 171 | def local?; true end 172 | 173 | def to_s; 'hash_fs' end 174 | 175 | def tmp &block 176 | tmp_dir = "/tmp_#{rand(10**6)}" 177 | create_dir tmp_dir 178 | if block 179 | begin 180 | block.call tmp_dir 181 | ensure 182 | delete_dir tmp_dir 183 | end 184 | else 185 | tmp_dir 186 | end 187 | end 188 | 189 | protected 190 | def assert obj, method, arg 191 | raise "#{obj} should #{method} #{arg}" unless obj.send method, arg 192 | end 193 | 194 | def assert_not obj, method, arg 195 | raise "#{obj} should not #{method} #{arg}" if obj.send method, arg 196 | end 197 | 198 | def split_path path 199 | parts = path[1..-1].split('/') 200 | parts.unshift '/' 201 | name = parts.pop 202 | return parts, name 203 | end 204 | 205 | def cd parts 206 | current = self 207 | iterator = parts.clone 208 | while iterator.first 209 | current = current[iterator.first] 210 | iterator.shift 211 | end 212 | current 213 | end 214 | end 215 | end 216 | end -------------------------------------------------------------------------------- /docs/site/ssh_deployment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ssh_deployment.rb 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 59 | 60 | 185 |
186 | 187 | 188 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | **Documentation:** http://alexeypetrushin.github.com/vfs 2 | 3 | Virtual File System provides **clean, simple and unified API over different storages** (Local File System, AWS S3, SFTP, ...). 4 | 5 | - very simple and intuitive API. 6 | - same API for different storages. 7 | - work simultaneously with multiple storages. 8 | - small codebase, easy to learn and extend. 9 | - driver implementation is very simple, it is easy to create new drivers. 10 | 11 | Such unified API is possible because although the API of storages are different the core concept are almost the same. 12 | 13 | Install Vfs with Rubygems: 14 | 15 | gem install vfs 16 | 17 | Once installed, You can proceed with the [basic example][basics], there's also [S3 version][s3_basics] and [SFTP version][ssh_basics] (also [S3 backup][s3_backup] and [SSH/SFTP deployment][ssh_deployment] examples availiable). 18 | 19 | You can report bugs and discuss features on the [issues page][issues]. 20 | 21 | ## Sample 22 | 23 | ``` ruby 24 | # Preparing sandbox for our sample and cleaning it before starting 25 | # (ignore the `$sandbox` variable, it's needed to reuse this code in S3 and SSH samples). 26 | require 'vfs' 27 | sandbox = $sandbox || '/tmp/vfs_sandbox'.to_dir.delete 28 | 29 | # Creating simple Hello World project. 30 | project = sandbox['hello_world'] 31 | 32 | # Writing readme file (note that parent dirs where created automatically). 33 | project['readme.txt'].write 'My App' 34 | 35 | # We can assign files and dirs to variables, now the `readme` variable refers to our 36 | # readme.txt file. 37 | readme = project['readme.txt'] 38 | 39 | # Let's ensure that it's all ok with our readme file and check its attributes. 40 | p readme.name # => readme.txt 41 | p [readme.basename, readme.extension] # => ['readme', 'txt'] 42 | p readme.path # => /.../readme.txt 43 | p readme.exist? # => true 44 | p readme.file? # => true 45 | p readme.dir? # => false 46 | p readme.size # => 6 47 | p readme.created_at # => 2011-09-09 13:20:43 +0400 48 | p readme.updated_at # => 2011-09-09 13:20:43 +0400 49 | 50 | # Reading - You can read all at once or do it sequentially (input stream 51 | # will be automatically splitted into chunks of reasonable size). 52 | p readme.read # => "My shiny App" 53 | readme.read{|chunk| p chunk} # => "My shiny App" 54 | 55 | # The same for writing - write all at once or do it sequentially 56 | # (if there's no file it will be created, if it exists it will be rewriten). 57 | readme.write "My App v2" 58 | readme.write{|stream| stream.write "My App v3"} 59 | p readme.read # => "My shiny App v3" 60 | 61 | # Appending content to existing file. 62 | readme.append "How to install ..." 63 | p readme.size # => 27 64 | 65 | # Copying and Moving. It also works exactly the same 66 | # way if You copy or move files and dirs to other storages. 67 | readme.copy_to project['docs/readme.txt'] 68 | p project['docs/readme.txt'].exist? # => true 69 | p readme.exist? # => true 70 | 71 | readme.move_to project['docs/readme.txt'] 72 | p project['docs/readme.txt'].exist? # => true 73 | p readme.exist? # => false 74 | 75 | # Let's add empty Rakefile to our project. 76 | project['Rakefile'].write 77 | 78 | # Operations with directories - checking our project exists and not empty. 79 | p project.exist? # => true 80 | p project.empty? # => false 81 | 82 | # Listing dir content. There are two versions of methods - 83 | # without block the result will be Array of Entries, with block 84 | # it will iterate over directory sequentially. 85 | p project.entries # => [/.../docs, /.../Rakefile] 86 | p project.files # => [/.../Rakefile] 87 | p project.dirs # => [/.../docs] 88 | project.entries do |entry| # => ["docs", false] 89 | p [entry.name, entry.file?] # => ["Rakefile", true] 90 | end 91 | p project.include?('Rakefile') # => true 92 | 93 | # You can also use glob (if storage support it). 94 | if project.driver.local? 95 | p project.entries('**/Rake*') # => [/.../Rakefile] 96 | p project['**/Rake*'] # => [/.../Rakefile] 97 | end 98 | 99 | # The result of dir listing is just an array of Entries, so 100 | # You can use it to do interesting things. For example this code will 101 | # calculates the size of sources in our project. 102 | if project.driver.local? 103 | project['**/*.rb'].collect(&:size).reduce(0, :+) 104 | end 105 | 106 | # Copying and moving - let's create another project by cloning our hello_world. 107 | project.copy_to sandbox['another_project'] 108 | p sandbox['another_project'].entries # => [/.../docs, .../Rakefile] 109 | 110 | # Cleaning sandbox. 111 | sandbox.delete 112 | ``` 113 | 114 | ## Integration with [Vos][vos] (Virtual Operating System) 115 | 116 | Vfs can be used toghether with the Virtual Operating System Tool, and while the Vfs covers all the I/O operations the Vos provides support for remote command execution. 117 | You can use this combination to fully control remote machines, for example - I'm using it to manage my production servers (setup, administration, deployment, migration, ...). 118 | 119 | For more details please go to [Vos][vos] project page. 120 | You can also take look at the actual configuration I'm using to control my servers [My Cluster][my_cluster] (in conjunction with small configuration tool [Cluster Management][cluster_management]). 121 | 122 | # Why? 123 | 124 | To easy my work: with local FS, remote FS, and some specific systems like Hadoop DFS. 125 | 126 | Because the API of standard File/Dir/FileUtils classes are just terrible. And there's the reason for it - the goal of thouse tools is to provide 1-to-1 clone of underlying OS API, instead of provididing handy tool. 127 | 128 | And if you want to use remote FS - things are getting even worse and more complicated (Net::SSH & Net::SFTP use a little 129 | different API than local FS, and you has to remember all thouse little quirks). 130 | 131 | # Generate documentation 132 | 133 | `cd docs && docco -o site *.rb` 134 | 135 | # Contributors 136 | 137 | - [Alexey Petrushin](https://github.com/alexeypetrushin) 138 | - [momolog](https://github.com/momolog) 139 | 140 | ## License 141 | 142 | Copyright (c) Alexey Petrushin http://petrush.in, released under the MIT license. 143 | 144 | [vos]: http://github.com/alexeypetrushin/vos 145 | [cluster_management]: http://github.com/alexeypetrushin/cluster_management 146 | [my_cluster]: http://github.com/alexeypetrushin/my_cluster 147 | 148 | [basics]: http://alexeypetrushin.github.com/vfs/basics.html 149 | [s3_basics]: http://alexeypetrushin.github.com/vfs/s3_basics.html 150 | [s3_backup]: http://alexeypetrushin.github.com/vfs/s3_backup.html 151 | [ssh_basics]: http://alexeypetrushin.github.com/vfs/ssh_basics.html 152 | [ssh_deployment]: http://alexeypetrushin.github.com/vfs/ssh_deployment.html 153 | [issues]: https://github.com/alexeypetrushin/vfs/issues -------------------------------------------------------------------------------- /lib/vfs/entries/dir.rb: -------------------------------------------------------------------------------- 1 | module Vfs 2 | class Dir < Entry 3 | 4 | # Container. 5 | def [] path 6 | path = path.to_s 7 | if path =~ /.+[\/]$/ 8 | path = path.sub /\/$/, '' 9 | dir path 10 | elsif path =~ /\*/ 11 | entries path 12 | else 13 | entry path 14 | end 15 | end 16 | alias_method :/, :[] 17 | 18 | 19 | # Attributes. 20 | alias_method :exist?, :dir? 21 | 22 | # CRUD. 23 | 24 | def create options = {} 25 | driver.open do 26 | try = 0 27 | begin 28 | try += 1 29 | driver.create_dir path 30 | rescue StandardError => error 31 | entry = self.entry 32 | attrs = entry.get 33 | if attrs and attrs[:file] #entry.exist? 34 | entry.delete 35 | elsif attrs and attrs[:dir] 36 | # dir already exist, no need to recreate it 37 | return self 38 | else 39 | parent = self.parent 40 | if parent.exist? 41 | # some unknown error 42 | raise error 43 | else 44 | parent.create(options) 45 | end 46 | end 47 | 48 | try < 2 ? retry : raise(error) 49 | end 50 | end 51 | self 52 | end 53 | 54 | def delete options = {} 55 | delete_entry :dir, :file 56 | end 57 | 58 | # Content. 59 | 60 | def entries *args, &block 61 | raise "invalid arguments #{args.inspect}!" if args.size > 2 62 | options = args.last.is_a?(Hash) ? args.pop : {} 63 | query = args.first 64 | options[:bang] = true unless options.include? :bang 65 | filter = options[:filter] 66 | type_required = options[:type] 67 | 68 | driver.open do 69 | begin 70 | list = [] 71 | # Query option is optional and supported only for some drivers (local driver for example). 72 | driver.each_entry path, query do |name, type| 73 | # For performance reasons some drivers may return the type of entry as 74 | # optionally evaluated callback. 75 | type = type.call if (filter or type_required) and type.is_a?(Proc) 76 | 77 | next if name == '' # Fix for https://github.com/alexeypetrushin/vfs/issues/5 78 | next if filter and (filter != type) 79 | 80 | entry = if type == :dir 81 | dir(name) 82 | elsif type == :file 83 | file(name) 84 | else 85 | entry(name) 86 | end 87 | block ? block.call(entry) : (list << entry) 88 | end 89 | block ? nil : list 90 | rescue StandardError => error 91 | attrs = get 92 | if attrs and attrs[:file] 93 | raise Error, "can't query entries on File ('#{self}')!" 94 | elsif attrs and attrs[:dir] 95 | # Some unknown error. 96 | raise error 97 | else 98 | # TODO2 remove :bang. 99 | raise Error, "'#{self}' not exist!" if options[:bang] 100 | [] 101 | end 102 | end 103 | end 104 | end 105 | alias_method :each, :entries 106 | 107 | def files *args, &block 108 | options = args.last.is_a?(Hash) ? args.pop : {} 109 | 110 | options[:filter] = :file 111 | args << options 112 | entries *args, &block 113 | end 114 | 115 | def dirs *args, &block 116 | options = args.last.is_a?(Hash) ? args.pop : {} 117 | 118 | options[:filter] = :dir 119 | args << options 120 | entries *args, &block 121 | end 122 | 123 | def include? name 124 | entry[name].exist? 125 | end 126 | alias_method :has?, :include? 127 | 128 | def empty? 129 | catch :break do 130 | entries{|e| throw :break, false} 131 | true 132 | end 133 | end 134 | 135 | # Transfers. 136 | 137 | def copy_to to, options = {} 138 | options[:bang] = true unless options.include? :bang 139 | 140 | raise Error, "invalid argument, destination should be a Entry (#{to})!" unless to.is_a? Entry 141 | raise Error, "you can't copy to itself" if self == to 142 | 143 | target = if to.is_a? File 144 | to.dir 145 | elsif to.is_a? Dir 146 | to.dir 147 | elsif to.is_a? UniversalEntry 148 | to.dir 149 | else 150 | raise "can't copy to unknown Entry!" 151 | end 152 | 153 | # efficient_dir_copy(target, options) || unefficient_dir_copy(target, options) 154 | unefficient_dir_copy(target, options) 155 | 156 | target 157 | end 158 | 159 | def move_to to, options = {} 160 | copy_to to, options 161 | delete options 162 | to 163 | end 164 | 165 | protected 166 | def unefficient_dir_copy to, options 167 | to.create options 168 | entries options.merge(type: true) do |e| 169 | if e.is_a? Dir 170 | e.copy_to to.dir(e.name), options 171 | elsif e.is_a? File 172 | e.copy_to to.file(e.name), options 173 | else 174 | raise 'internal error' 175 | end 176 | end 177 | end 178 | 179 | # def efficient_dir_copy to, options 180 | # return false if self.class.dont_use_efficient_dir_copy 181 | # 182 | # driver.open do 183 | # try = 0 184 | # begin 185 | # try += 1 186 | # self.class.efficient_dir_copy(self, to, options[:override]) 187 | # rescue StandardError => error 188 | # unknown_errors = 0 189 | # 190 | # attrs = get 191 | # if attrs and attrs[:file] 192 | # raise Error, "can't copy File as a Dir ('#{self}')!" 193 | # elsif attrs and attrs[:dir] 194 | # # some unknown error (but it also maybe caused by to be fixed error in 'to') 195 | # unknown_errors += 1 196 | # else 197 | # raise Error, "'#{self}' not exist!" if options[:bang] 198 | # return true 199 | # end 200 | # 201 | # attrs = to.get 202 | # if attrs and attrs[:file] 203 | # if options[:override] 204 | # to.delete 205 | # else 206 | # raise Vfs::Error, "entry #{to} already exist!" 207 | # end 208 | # elsif attrs and attrs[:dir] 209 | # unknown_errors += 1 210 | # # if options[:override] 211 | # # to.delete 212 | # # else 213 | # # dir_already_exist = true 214 | # # # raise Vfs::Error, "entry #{to} already exist!" 215 | # # end 216 | # else # parent not exist 217 | # parent = to.parent 218 | # if parent.exist? 219 | # # some unknown error (but it also maybe caused by already fixed error in 'from') 220 | # unknown_errors += 1 221 | # else 222 | # parent.create(options) 223 | # end 224 | # end 225 | # 226 | # raise error if unknown_errors > 1 227 | # try < 2 ? retry : raise(error) 228 | # end 229 | # end 230 | # end 231 | # 232 | # def self.efficient_dir_copy from, to, override 233 | # from.driver.open{ 234 | # driver.respond_to?(:efficient_dir_copy) and driver.efficient_dir_copy(from, to, override) 235 | # } or 236 | # to.driver.open{ 237 | # driver.respond_to?(:efficient_dir_copy) and driver.efficient_dir_copy(from, to, override) 238 | # } 239 | # end 240 | end 241 | end -------------------------------------------------------------------------------- /spec/dir_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'set' 3 | 4 | describe 'Dir' do 5 | with_test_dir 6 | 7 | before do 8 | @path = test_dir['a/b/c'] 9 | end 10 | 11 | describe 'existence' do 12 | it "should check only dirs" do 13 | @path.should_not exist 14 | @path.file.create 15 | @path.should be_file 16 | @path.dir.should_not exist 17 | @path.dir.create 18 | @path.should be_dir 19 | @path.dir.should exist 20 | end 21 | end 22 | 23 | it "should not respond to read and write methods" do 24 | -> {@path.dir.read}.should raise_error(NoMethodError) 25 | -> {@path.dir.write}.should raise_error(NoMethodError) 26 | end 27 | 28 | describe 'create' do 29 | it 'should be chainable' do 30 | @path.dir.create.should == @path 31 | end 32 | 33 | it 'should create parent dirs if not exists' do 34 | @path.parent.should_not exist 35 | @path.dir.create 36 | @path.should be_dir 37 | end 38 | 39 | it 'should silently exit if dir already exist' do 40 | @path.dir.create 41 | @path.dir.create 42 | end 43 | 44 | it 'should override existing file' do 45 | @path.file.create 46 | @path.should be_file 47 | @path.dir.create 48 | @path.should be_dir 49 | end 50 | 51 | it 'should not override existing dir with content' do 52 | dir = @path.dir 53 | dir.create 54 | file = dir.file :file 55 | file.create 56 | file.should exist 57 | 58 | dir.create 59 | file.should exist 60 | end 61 | end 62 | 63 | describe 'deleting' do 64 | it "should delete a file" do 65 | @path.file.create 66 | @path.dir.delete 67 | @path.entry.should_not exist 68 | end 69 | 70 | it "shouldn't raise if dir not exist" do 71 | @path.dir.delete 72 | end 73 | 74 | it 'should delete recursivelly' do 75 | dir = @path.dir 76 | dir.create 77 | dir.file(:file).write 'something' 78 | dir.dir(:dir).create.tap do |dir| 79 | dir.file(:file2).write 'something2' 80 | end 81 | 82 | dir.delete 83 | dir.should_not exist 84 | end 85 | 86 | it 'should be chainable' do 87 | @path.dir.delete.should == @path 88 | end 89 | end 90 | 91 | describe 'entries, files, dirs' do 92 | before do 93 | @path.dir('dir').create 94 | @path.dir('dir/another_dir').create 95 | @path.file('file').create 96 | end 97 | 98 | it 'entries' do 99 | -> {@path['non_existing'].entries}.should raise_error(Vfs::Error, /not exist/) 100 | @path['non_existing'].entries(bang: false).should == [] 101 | @path.entries.to_set.should be_eql([@path.entry('dir'), @path.entry('file')].to_set) 102 | list = [] 103 | @path.entries{|e| list << e} 104 | list.to_set.should be_eql([@path.entry('dir'), @path.entry('file')].to_set) 105 | end 106 | 107 | it 'entries with type' do 108 | @path.entries(type: true).to_set.should be_eql([@path.dir('dir'), @path.file('file')].to_set) 109 | end 110 | 111 | it "glob search support" do 112 | @path.dir('dir_a').create 113 | @path.file('file_a').create 114 | @path.dir('dir_b').create 115 | @path.entries('*_a').collect(&:name).sort.should == %w(dir_a file_a) 116 | end 117 | 118 | it 'should raise error if trying :entries on file' do 119 | @path.file('some_file').create 120 | -> {@path.dir('some_file').entries}.should raise_error(/File/) 121 | end 122 | 123 | it 'files' do 124 | @path.files.should be_eql([@path.file('file')]) 125 | end 126 | 127 | it 'dirs' do 128 | @path.dirs.should be_eql([@path.dir('dir')]) 129 | end 130 | 131 | it 'has? & include?' do 132 | @path.include?('dir').should be_true 133 | @path.include?('dir/another_dir').should be_true 134 | @path.include?('file').should be_true 135 | @path.include?('non_existing').should be_false 136 | end 137 | 138 | it 'empty?' do 139 | @path.empty?.should be_false 140 | @path.dir('empty_dir').create.empty?.should be_true 141 | end 142 | 143 | it "should threat ['**/*.rb'] as glob" do 144 | @path['**/*nother*'].first.name.should == 'another_dir' 145 | end 146 | end 147 | 148 | describe 'copying' do 149 | before do 150 | @from = @path.dir 151 | @from.create 152 | @from.file('file').write 'something' 153 | @from.dir('dir').create.tap do |dir| 154 | dir.file('file2').write 'something2' 155 | end 156 | end 157 | 158 | it 'should not copy to itself' do 159 | -> {@from.copy_to @from}.should raise_error(Vfs::Error, /itself/) 160 | end 161 | 162 | shared_examples_for 'copy_to behavior' do 163 | it 'should copy to file and overwrite it' do 164 | @from.copy_to @to.file 165 | @to['file'].read.should == 'something' 166 | end 167 | 168 | it 'should override files' do 169 | @from.copy_to @to 170 | 171 | @from['dir/file2'].write 'another' 172 | @from.copy_to @to 173 | @to['dir/file2'].read.should == 'another' 174 | end 175 | 176 | it 'should copy to UniversalEntry (and overwrite)' do 177 | @from.copy_to @to.entry 178 | 179 | @from.copy_to @to.entry 180 | @to['file'].read.should == 'something' 181 | end 182 | 183 | it "shouldn't delete existing content of directory" do 184 | @to.dir.create 185 | @to.file('existing_file').write 'existing_content' 186 | @to.dir('existing_dir').create 187 | @to.file('dir/existing_file2').write 'existing_content2' 188 | 189 | @from.copy_to @to 190 | # copied files 191 | @to['file'].read.should == 'something' 192 | @to['dir/file2'].read.should == 'something2' 193 | 194 | # Shouldn't delete already existing files. 195 | @to.file('existing_file').read.should == 'existing_content' 196 | @to.dir('existing_dir').should exist 197 | @to.file('dir/existing_file2').read.should == 'existing_content2' 198 | end 199 | 200 | it 'should be chainable' do 201 | @from.copy_to(@to).should == @to 202 | end 203 | 204 | it "should override without deleting other files" do 205 | @from.copy_to(@to).should == @to 206 | @to.file('other_file').write 'other' 207 | 208 | @from.copy_to(@to).should == @to 209 | @to.file('other_file').read.should == 'other' 210 | end 211 | 212 | it "should raise error if try to copy file as dir" do 213 | dir = @from.dir 'file' 214 | dir.file?.should be_true 215 | -> {dir.copy_to @to}.should raise_error(Vfs::Error) 216 | end 217 | end 218 | 219 | describe 'general copy' do 220 | it_should_behave_like 'copy_to behavior' 221 | 222 | before do 223 | # Prevenging usage of :efficient_dir_copy. 224 | # Vfs::Dir.dont_use_efficient_dir_copy = true 225 | 226 | @to = test_dir['to'] 227 | end 228 | # after do 229 | # Vfs::Dir.dont_use_efficient_dir_copy = false 230 | # end 231 | end 232 | 233 | # describe 'effective copy' do 234 | # it_should_behave_like 'copy_to behavior' 235 | # 236 | # before do 237 | # @to = test_dir['to'] 238 | # end 239 | # end 240 | end 241 | 242 | describe 'moving' do 243 | it 'move_to' do 244 | from, to = @path.file('from'), @path.file('to') 245 | from.should_receive(:copy_to).with(to) 246 | from.should_receive(:delete).with() 247 | from.move_to to 248 | end 249 | 250 | it 'should be chainable' do 251 | from, to = @path.dir('from').create, @path.dir('to') 252 | from.move_to(to).should == to 253 | end 254 | end 255 | end -------------------------------------------------------------------------------- /docs/site/public/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /* 8 | * Corrects `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | 25 | /* 26 | * Corrects `inline-block` display not defined in IE 8/9. 27 | */ 28 | 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | } 34 | 35 | /* 36 | * Prevents modern browsers from displaying `audio` without controls. 37 | * Remove excess height in iOS 5 devices. 38 | */ 39 | 40 | audio:not([controls]) { 41 | display: none; 42 | height: 0; 43 | } 44 | 45 | /* 46 | * Addresses styling for `hidden` attribute not present in IE 8/9. 47 | */ 48 | 49 | [hidden] { 50 | display: none; 51 | } 52 | 53 | /* ========================================================================== 54 | Base 55 | ========================================================================== */ 56 | 57 | /* 58 | * 1. Sets default font family to sans-serif. 59 | * 2. Prevents iOS text size adjust after orientation change, without disabling 60 | * user zoom. 61 | */ 62 | 63 | html { 64 | font-family: sans-serif; /* 1 */ 65 | -webkit-text-size-adjust: 100%; /* 2 */ 66 | -ms-text-size-adjust: 100%; /* 2 */ 67 | } 68 | 69 | /* 70 | * Removes default margin. 71 | */ 72 | 73 | body { 74 | margin: 0; 75 | } 76 | 77 | /* ========================================================================== 78 | Links 79 | ========================================================================== */ 80 | 81 | /* 82 | * Addresses `outline` inconsistency between Chrome and other browsers. 83 | */ 84 | 85 | a:focus { 86 | outline: thin dotted; 87 | } 88 | 89 | /* 90 | * Improves readability when focused and also mouse hovered in all browsers. 91 | */ 92 | 93 | a:active, 94 | a:hover { 95 | outline: 0; 96 | } 97 | 98 | /* ========================================================================== 99 | Typography 100 | ========================================================================== */ 101 | 102 | /* 103 | * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, 104 | * Safari 5, and Chrome. 105 | */ 106 | 107 | h1 { 108 | font-size: 2em; 109 | } 110 | 111 | /* 112 | * Addresses styling not present in IE 8/9, Safari 5, and Chrome. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: 1px dotted; 117 | } 118 | 119 | /* 120 | * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: bold; 126 | } 127 | 128 | /* 129 | * Addresses styling not present in Safari 5 and Chrome. 130 | */ 131 | 132 | dfn { 133 | font-style: italic; 134 | } 135 | 136 | /* 137 | * Addresses styling not present in IE 8/9. 138 | */ 139 | 140 | mark { 141 | background: #ff0; 142 | color: #000; 143 | } 144 | 145 | 146 | /* 147 | * Corrects font family set oddly in Safari 5 and Chrome. 148 | */ 149 | 150 | code, 151 | kbd, 152 | pre, 153 | samp { 154 | font-family: monospace, serif; 155 | font-size: 1em; 156 | } 157 | 158 | /* 159 | * Improves readability of pre-formatted text in all browsers. 160 | */ 161 | 162 | pre { 163 | white-space: pre; 164 | white-space: pre-wrap; 165 | word-wrap: break-word; 166 | } 167 | 168 | /* 169 | * Sets consistent quote types. 170 | */ 171 | 172 | q { 173 | quotes: "\201C" "\201D" "\2018" "\2019"; 174 | } 175 | 176 | /* 177 | * Addresses inconsistent and variable font size in all browsers. 178 | */ 179 | 180 | small { 181 | font-size: 80%; 182 | } 183 | 184 | /* 185 | * Prevents `sub` and `sup` affecting `line-height` in all browsers. 186 | */ 187 | 188 | sub, 189 | sup { 190 | font-size: 75%; 191 | line-height: 0; 192 | position: relative; 193 | vertical-align: baseline; 194 | } 195 | 196 | sup { 197 | top: -0.5em; 198 | } 199 | 200 | sub { 201 | bottom: -0.25em; 202 | } 203 | 204 | /* ========================================================================== 205 | Embedded content 206 | ========================================================================== */ 207 | 208 | /* 209 | * Removes border when inside `a` element in IE 8/9. 210 | */ 211 | 212 | img { 213 | border: 0; 214 | } 215 | 216 | /* 217 | * Corrects overflow displayed oddly in IE 9. 218 | */ 219 | 220 | svg:not(:root) { 221 | overflow: hidden; 222 | } 223 | 224 | /* ========================================================================== 225 | Figures 226 | ========================================================================== */ 227 | 228 | /* 229 | * Addresses margin not present in IE 8/9 and Safari 5. 230 | */ 231 | 232 | figure { 233 | margin: 0; 234 | } 235 | 236 | /* ========================================================================== 237 | Forms 238 | ========================================================================== */ 239 | 240 | /* 241 | * Define consistent border, margin, and padding. 242 | */ 243 | 244 | fieldset { 245 | border: 1px solid #c0c0c0; 246 | margin: 0 2px; 247 | padding: 0.35em 0.625em 0.75em; 248 | } 249 | 250 | /* 251 | * 1. Corrects color not being inherited in IE 8/9. 252 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 253 | */ 254 | 255 | legend { 256 | border: 0; /* 1 */ 257 | padding: 0; /* 2 */ 258 | } 259 | 260 | /* 261 | * 1. Corrects font family not being inherited in all browsers. 262 | * 2. Corrects font size not being inherited in all browsers. 263 | * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome 264 | */ 265 | 266 | button, 267 | input, 268 | select, 269 | textarea { 270 | font-family: inherit; /* 1 */ 271 | font-size: 100%; /* 2 */ 272 | margin: 0; /* 3 */ 273 | } 274 | 275 | /* 276 | * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in 277 | * the UA stylesheet. 278 | */ 279 | 280 | button, 281 | input { 282 | line-height: normal; 283 | } 284 | 285 | /* 286 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 287 | * and `video` controls. 288 | * 2. Corrects inability to style clickable `input` types in iOS. 289 | * 3. Improves usability and consistency of cursor style between image-type 290 | * `input` and others. 291 | */ 292 | 293 | button, 294 | html input[type="button"], /* 1 */ 295 | input[type="reset"], 296 | input[type="submit"] { 297 | -webkit-appearance: button; /* 2 */ 298 | cursor: pointer; /* 3 */ 299 | } 300 | 301 | /* 302 | * Re-set default cursor for disabled elements. 303 | */ 304 | 305 | button[disabled], 306 | input[disabled] { 307 | cursor: default; 308 | } 309 | 310 | /* 311 | * 1. Addresses box sizing set to `content-box` in IE 8/9. 312 | * 2. Removes excess padding in IE 8/9. 313 | */ 314 | 315 | input[type="checkbox"], 316 | input[type="radio"] { 317 | box-sizing: border-box; /* 1 */ 318 | padding: 0; /* 2 */ 319 | } 320 | 321 | /* 322 | * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. 323 | * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome 324 | * (include `-moz` to future-proof). 325 | */ 326 | 327 | input[type="search"] { 328 | -webkit-appearance: textfield; /* 1 */ 329 | -moz-box-sizing: content-box; 330 | -webkit-box-sizing: content-box; /* 2 */ 331 | box-sizing: content-box; 332 | } 333 | 334 | /* 335 | * Removes inner padding and search cancel button in Safari 5 and Chrome 336 | * on OS X. 337 | */ 338 | 339 | input[type="search"]::-webkit-search-cancel-button, 340 | input[type="search"]::-webkit-search-decoration { 341 | -webkit-appearance: none; 342 | } 343 | 344 | /* 345 | * Removes inner padding and border in Firefox 4+. 346 | */ 347 | 348 | button::-moz-focus-inner, 349 | input::-moz-focus-inner { 350 | border: 0; 351 | padding: 0; 352 | } 353 | 354 | /* 355 | * 1. Removes default vertical scrollbar in IE 8/9. 356 | * 2. Improves readability and alignment in all browsers. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; /* 1 */ 361 | vertical-align: top; /* 2 */ 362 | } 363 | 364 | /* ========================================================================== 365 | Tables 366 | ========================================================================== */ 367 | 368 | /* 369 | * Remove most spacing between table cells. 370 | */ 371 | 372 | table { 373 | border-collapse: collapse; 374 | border-spacing: 0; 375 | } -------------------------------------------------------------------------------- /docs/site/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Typography ----------------------------*/ 2 | 3 | @font-face { 4 | font-family: 'aller-light'; 5 | src: url('public/fonts/aller-light.eot'); 6 | src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'), 7 | url('public/fonts/aller-light.woff') format('woff'), 8 | url('public/fonts/aller-light.ttf') format('truetype'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: 'aller-bold'; 15 | src: url('public/fonts/aller-bold.eot'); 16 | src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'), 17 | url('public/fonts/aller-bold.woff') format('woff'), 18 | url('public/fonts/aller-bold.ttf') format('truetype'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | @font-face { 24 | font-family: 'novecento-bold'; 25 | src: url('public/fonts/novecento-bold.eot'); 26 | src: url('public/fonts/novecento-bold.eot?#iefix') format('embedded-opentype'), 27 | url('public/fonts/novecento-bold.woff') format('woff'), 28 | url('public/fonts/novecento-bold.ttf') format('truetype'); 29 | font-weight: normal; 30 | font-style: normal; 31 | } 32 | 33 | /*--------------------- Layout ----------------------------*/ 34 | html { height: 100%; } 35 | body { 36 | font-family: "aller-light"; 37 | font-size: 14px; 38 | line-height: 18px; 39 | color: #30404f; 40 | margin: 0; padding: 0; 41 | height:100%; 42 | } 43 | #container { min-height: 100%; } 44 | 45 | a { 46 | color: #000; 47 | } 48 | 49 | b, strong { 50 | font-weight: normal; 51 | font-family: "aller-bold"; 52 | } 53 | 54 | p { 55 | margin: 15px 0 0px; 56 | } 57 | .annotation ul, .annotation ol { 58 | margin: 25px 0; 59 | } 60 | .annotation ul li, .annotation ol li { 61 | font-size: 14px; 62 | line-height: 18px; 63 | margin: 10px 0; 64 | } 65 | 66 | h1, h2, h3, h4, h5, h6 { 67 | color: #112233; 68 | line-height: 1em; 69 | font-weight: normal; 70 | font-family: "novecento-bold"; 71 | text-transform: uppercase; 72 | margin: 30px 0 15px 0; 73 | } 74 | 75 | h1 { 76 | margin-top: 40px; 77 | } 78 | 79 | hr { 80 | border: 0; 81 | background: 1px #ddd; 82 | height: 1px; 83 | margin: 20px 0; 84 | } 85 | 86 | pre, tt, code { 87 | font-size: 12px; line-height: 16px; 88 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; 89 | margin: 0; padding: 0; 90 | } 91 | .annotation pre { 92 | display: block; 93 | margin: 0; 94 | padding: 7px 10px; 95 | background: #fcfcfc; 96 | -moz-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 97 | -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 98 | box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 99 | overflow-x: auto; 100 | } 101 | .annotation pre code { 102 | border: 0; 103 | padding: 0; 104 | background: transparent; 105 | } 106 | 107 | 108 | blockquote { 109 | border-left: 5px solid #ccc; 110 | margin: 0; 111 | padding: 1px 0 1px 1em; 112 | } 113 | .sections blockquote p { 114 | font-family: Menlo, Consolas, Monaco, monospace; 115 | font-size: 12px; line-height: 16px; 116 | color: #999; 117 | margin: 10px 0 0; 118 | white-space: pre-wrap; 119 | } 120 | 121 | ul.sections { 122 | list-style: none; 123 | padding:0 0 5px 0;; 124 | margin:0; 125 | } 126 | 127 | /* 128 | Force border-box so that % widths fit the parent 129 | container without overlap because of margin/padding. 130 | 131 | More Info : http://www.quirksmode.org/css/box.html 132 | */ 133 | ul.sections > li > div { 134 | -moz-box-sizing: border-box; /* firefox */ 135 | -ms-box-sizing: border-box; /* ie */ 136 | -webkit-box-sizing: border-box; /* webkit */ 137 | -khtml-box-sizing: border-box; /* konqueror */ 138 | box-sizing: border-box; /* css3 */ 139 | } 140 | 141 | 142 | /*---------------------- Jump Page -----------------------------*/ 143 | #jump_to, #jump_page { 144 | margin: 0; 145 | background: white; 146 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 147 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 148 | font: 16px Arial; 149 | cursor: pointer; 150 | text-align: right; 151 | list-style: none; 152 | } 153 | 154 | #jump_to a { 155 | text-decoration: none; 156 | } 157 | 158 | #jump_to a.large { 159 | display: none; 160 | } 161 | #jump_to a.small { 162 | font-size: 22px; 163 | font-weight: bold; 164 | color: #676767; 165 | } 166 | 167 | #jump_to, #jump_wrapper { 168 | position: fixed; 169 | right: 0; top: 0; 170 | padding: 10px 15px; 171 | margin:0; 172 | } 173 | 174 | #jump_wrapper { 175 | display: none; 176 | padding:0; 177 | } 178 | 179 | #jump_to:hover #jump_wrapper { 180 | display: block; 181 | } 182 | 183 | #jump_page { 184 | padding: 5px 0 3px; 185 | margin: 0 0 25px 25px; 186 | } 187 | 188 | #jump_page .source { 189 | display: block; 190 | padding: 15px; 191 | text-decoration: none; 192 | border-top: 1px solid #eee; 193 | } 194 | 195 | #jump_page .source:hover { 196 | background: #f5f5ff; 197 | } 198 | 199 | #jump_page .source:first-child { 200 | } 201 | 202 | /*---------------------- Low resolutions (> 320px) ---------------------*/ 203 | @media only screen and (min-width: 320px) { 204 | .pilwrap { display: none; } 205 | 206 | ul.sections > li > div { 207 | display: block; 208 | padding:5px 10px 0 10px; 209 | } 210 | 211 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 212 | padding-left: 30px; 213 | } 214 | 215 | ul.sections > li > div.content { 216 | overflow-x:auto; 217 | -webkit-box-shadow: inset 0 0 5px #e5e5ee; 218 | box-shadow: inset 0 0 5px #e5e5ee; 219 | border: 1px solid #dedede; 220 | margin:5px 10px 5px 10px; 221 | padding-bottom: 5px; 222 | } 223 | 224 | ul.sections > li > div.annotation pre { 225 | margin: 7px 0 7px; 226 | padding-left: 15px; 227 | } 228 | 229 | ul.sections > li > div.annotation p tt, .annotation code { 230 | background: #f8f8ff; 231 | border: 1px solid #dedede; 232 | font-size: 12px; 233 | padding: 0 0.2em; 234 | } 235 | } 236 | 237 | /*---------------------- (> 481px) ---------------------*/ 238 | @media only screen and (min-width: 481px) { 239 | #container { 240 | position: relative; 241 | } 242 | body { 243 | background-color: #F5F5FF; 244 | font-size: 15px; 245 | line-height: 21px; 246 | } 247 | pre, tt, code { 248 | line-height: 18px; 249 | } 250 | p, ul, ol { 251 | margin: 0 0 15px; 252 | } 253 | 254 | 255 | #jump_to { 256 | padding: 5px 10px; 257 | } 258 | #jump_wrapper { 259 | padding: 0; 260 | } 261 | #jump_to, #jump_page { 262 | font: 10px Arial; 263 | text-transform: uppercase; 264 | } 265 | #jump_page .source { 266 | padding: 5px 10px; 267 | } 268 | #jump_to a.large { 269 | display: inline-block; 270 | } 271 | #jump_to a.small { 272 | display: none; 273 | } 274 | 275 | 276 | 277 | #background { 278 | position: absolute; 279 | top: 0; bottom: 0; 280 | width: 350px; 281 | background: #fff; 282 | border-right: 1px solid #e5e5ee; 283 | z-index: -1; 284 | } 285 | 286 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 287 | padding-left: 40px; 288 | } 289 | 290 | ul.sections > li { 291 | white-space: nowrap; 292 | } 293 | 294 | ul.sections > li > div { 295 | display: inline-block; 296 | } 297 | 298 | ul.sections > li > div.annotation { 299 | max-width: 350px; 300 | min-width: 350px; 301 | min-height: 5px; 302 | padding: 13px; 303 | overflow-x: hidden; 304 | white-space: normal; 305 | vertical-align: top; 306 | text-align: left; 307 | } 308 | ul.sections > li > div.annotation pre { 309 | margin: 15px 0 15px; 310 | padding-left: 15px; 311 | } 312 | 313 | ul.sections > li > div.content { 314 | padding: 13px; 315 | vertical-align: top; 316 | border: none; 317 | -webkit-box-shadow: none; 318 | box-shadow: none; 319 | } 320 | 321 | .pilwrap { 322 | position: relative; 323 | display: inline; 324 | } 325 | 326 | .pilcrow { 327 | font: 12px Arial; 328 | text-decoration: none; 329 | color: #454545; 330 | position: absolute; 331 | top: 3px; left: -20px; 332 | padding: 1px 2px; 333 | opacity: 0; 334 | -webkit-transition: opacity 0.2s linear; 335 | } 336 | .for-h1 .pilcrow { 337 | top: 47px; 338 | } 339 | .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { 340 | top: 35px; 341 | } 342 | 343 | ul.sections > li > div.annotation:hover .pilcrow { 344 | opacity: 1; 345 | } 346 | } 347 | 348 | /*---------------------- (> 1025px) ---------------------*/ 349 | @media only screen and (min-width: 1025px) { 350 | 351 | body { 352 | font-size: 16px; 353 | line-height: 24px; 354 | } 355 | 356 | #background { 357 | width: 525px; 358 | } 359 | ul.sections > li > div.annotation { 360 | max-width: 525px; 361 | min-width: 525px; 362 | padding: 10px 25px 1px 50px; 363 | } 364 | ul.sections > li > div.content { 365 | padding: 9px 15px 16px 25px; 366 | } 367 | } 368 | 369 | /*---------------------- Syntax Highlighting -----------------------------*/ 370 | 371 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 372 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 373 | /* 374 | 375 | github.com style (c) Vasily Polovnyov 376 | 377 | */ 378 | 379 | pre code { 380 | display: block; padding: 0.5em; 381 | color: #000; 382 | background: #f8f8ff 383 | } 384 | 385 | pre .hljs-comment, 386 | pre .hljs-template_comment, 387 | pre .hljs-diff .hljs-header, 388 | pre .hljs-javadoc { 389 | color: #408080; 390 | font-style: italic 391 | } 392 | 393 | pre .hljs-keyword, 394 | pre .hljs-assignment, 395 | pre .hljs-literal, 396 | pre .hljs-css .hljs-rule .hljs-keyword, 397 | pre .hljs-winutils, 398 | pre .hljs-javascript .hljs-title, 399 | pre .hljs-lisp .hljs-title, 400 | pre .hljs-subst { 401 | color: #954121; 402 | /*font-weight: bold*/ 403 | } 404 | 405 | pre .hljs-number, 406 | pre .hljs-hexcolor { 407 | color: #40a070 408 | } 409 | 410 | pre .hljs-string, 411 | pre .hljs-tag .hljs-value, 412 | pre .hljs-phpdoc, 413 | pre .hljs-tex .hljs-formula { 414 | color: #219161; 415 | } 416 | 417 | pre .hljs-title, 418 | pre .hljs-id { 419 | color: #19469D; 420 | } 421 | pre .hljs-params { 422 | color: #00F; 423 | } 424 | 425 | pre .hljs-javascript .hljs-title, 426 | pre .hljs-lisp .hljs-title, 427 | pre .hljs-subst { 428 | font-weight: normal 429 | } 430 | 431 | pre .hljs-class .hljs-title, 432 | pre .hljs-haskell .hljs-label, 433 | pre .hljs-tex .hljs-command { 434 | color: #458; 435 | font-weight: bold 436 | } 437 | 438 | pre .hljs-tag, 439 | pre .hljs-tag .hljs-title, 440 | pre .hljs-rules .hljs-property, 441 | pre .hljs-django .hljs-tag .hljs-keyword { 442 | color: #000080; 443 | font-weight: normal 444 | } 445 | 446 | pre .hljs-attribute, 447 | pre .hljs-variable, 448 | pre .hljs-instancevar, 449 | pre .hljs-lisp .hljs-body { 450 | color: #008080 451 | } 452 | 453 | pre .hljs-regexp { 454 | color: #B68 455 | } 456 | 457 | pre .hljs-class { 458 | color: #458; 459 | font-weight: bold 460 | } 461 | 462 | pre .hljs-symbol, 463 | pre .hljs-ruby .hljs-symbol .hljs-string, 464 | pre .hljs-ruby .hljs-symbol .hljs-keyword, 465 | pre .hljs-ruby .hljs-symbol .hljs-keymethods, 466 | pre .hljs-lisp .hljs-keyword, 467 | pre .hljs-tex .hljs-special, 468 | pre .hljs-input_number { 469 | color: #990073 470 | } 471 | 472 | pre .hljs-builtin, 473 | pre .hljs-constructor, 474 | pre .hljs-built_in, 475 | pre .hljs-lisp .hljs-title { 476 | color: #0086b3 477 | } 478 | 479 | pre .hljs-preprocessor, 480 | pre .hljs-pi, 481 | pre .hljs-doctype, 482 | pre .hljs-shebang, 483 | pre .hljs-cdata { 484 | color: #999; 485 | font-weight: bold 486 | } 487 | 488 | pre .hljs-deletion { 489 | background: #fdd 490 | } 491 | 492 | pre .hljs-addition { 493 | background: #dfd 494 | } 495 | 496 | pre .hljs-diff .hljs-change { 497 | background: #0086b3 498 | } 499 | 500 | pre .hljs-chunk { 501 | color: #aaa 502 | } 503 | 504 | pre .hljs-tex .hljs-formula { 505 | opacity: 0.5; 506 | } 507 | -------------------------------------------------------------------------------- /docs/site/basics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | basics.rb 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 59 | 60 | 407 |
408 | 409 | 410 | --------------------------------------------------------------------------------