├── .dockerignore ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── redmine ├── docker-compose.yml ├── gems.locked ├── gems.rb ├── lib ├── redmine-installer.rb └── redmine-installer │ ├── backup.rb │ ├── cli.rb │ ├── command.rb │ ├── configuration.rb │ ├── database.rb │ ├── easycheck.rb │ ├── environment.rb │ ├── errors.rb │ ├── install.rb │ ├── logger.rb │ ├── package.rb │ ├── package_config.rb │ ├── patches │ ├── ruby.rb │ └── tty.rb │ ├── profile.rb │ ├── redmine.rb │ ├── restore_db.rb │ ├── spec │ └── spec.rb │ ├── task.rb │ ├── task_module.rb │ ├── upgrade.rb │ ├── utils.rb │ └── version.rb ├── redmine-installer.gemspec └── spec ├── custom_matchers.rb ├── installer_helper.rb ├── installer_process.rb ├── lib ├── backup_restore_spec.rb ├── install_spec.rb └── upgrade_spec.rb ├── packages ├── high-installer-version.zip ├── redmine-3.3.0-bad-migration.zip ├── redmine-3.4.5-default-db.zip ├── redmine-3.4.5-rys.zip ├── redmine-3.4.5.zip ├── redmine-5.0.3.zip └── something-else.zip ├── packages_helper.rb ├── shared_contexts.rb └── spec_helper.rb /.dockerignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /.bundle 4 | /tmp 5 | /log.log 6 | /redmine-installer-err 7 | /redmine-installer-out 8 | /redmine 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-22.04 6 | steps: 7 | - uses: actions/checkout@v4 8 | - uses: isbang/compose-action@v1.4.1 9 | with: 10 | up-flags: --build --abort-on-container-exit --exit-code-from redmine_installer 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.gemtags 2 | /.tags 3 | /.idea 4 | *.gem 5 | *.rbc 6 | .bundle 7 | .config 8 | .yardoc 9 | InstalledFiles 10 | _yardoc 11 | coverage 12 | doc/ 13 | lib/bundler/man 14 | pkg 15 | rdoc 16 | spec/reports 17 | test/tmp 18 | test/version_tmp 19 | tmp 20 | *.bundle 21 | *.so 22 | *.o 23 | *.a 24 | mkmf.log 25 | test/* 26 | /log.log 27 | /redmine-installer-err 28 | /redmine-installer-out 29 | /redmine 30 | /vendor/* 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## [3.0.3] - 2023-01-16 5 | ### Fixed 6 | - rubygems < 3.3.12 still unable to install gem on some machines, try another way to define ruby dependency [#40](https://github.com/easyredmine/redmine-installer/issues/40) 7 | ## [3.0.2] - 2023-01-02 8 | ### Fixed 9 | - with rubygems < 3.3.12 unable to install gem [#40](https://github.com/easyredmine/redmine-installer/issues/40) 10 | ## [3.0.1] - 2022-12-14 11 | ### Fixed 12 | - do not show warning if Percona 13 | ## [3.0.0] - 2022-12-13 14 | ### Added 15 | - docker-compose for testing [#37](https://github.com/easyredmine/redmine-installer/pull/37) 16 | - .github workflow for CI 17 | - `env_check` for test server environment 18 | ### Fixed 19 | - ruby 3.x support [#35](https://github.com/easyredmine/redmine-installer/pull/35) 20 | - Psych aliases while parsing `database.yml` 21 | ### Changed 22 | - required ruby version = 3.1.2 [#36](https://github.com/easyredmine/redmine-installer/pull/36) 23 | - required redmine version = 5.0.4+ [#36](https://github.com/easyredmine/redmine-installer/pull/36) 24 | 25 | ## [2.4.0] - 2022-09-07 26 | ### Added 27 | - allow the administrator to manually empty the root directory [#33](https://github.com/easyredmine/redmine-installer/pull/33) 28 | 29 | ## [2.3.0.beta] - 2019-06-10 30 | 31 | ### Added 32 | - Gems are required via Bundler 33 | - Easycheck for server requirenments 34 | - Validation for the package for upgrading 35 | 36 | ### Changed 37 | - Gemfile was renamed to gems.rb 38 | - Installer is now asking if user wants to copy missing plugins 39 | - Tests aren't using system redmine-installer 40 | 41 | ### Deprecated 42 | - Ruby <= 2.3 43 | 44 | ### Removed 45 | 46 | ### Fixed 47 | 48 | ### Security 49 | 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.1.4-bullseye 2 | LABEL org.opencontainers.image.authors="support@easysoftware.com" 3 | RUN apt-get update && apt-get -y install lsb-release wget curl vim 4 | RUN wget https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.deb -O percona-release_latest_all.deb &&\ 5 | dpkg -i percona-release_latest_all.deb &&\ 6 | percona-release setup ps80 &&\ 7 | apt-get update && apt-get install -y libperconaserverclient21-dev percona-server-client 8 | WORKDIR /gem 9 | VOLUME ["/gem/vendor/"] 10 | COPY . ./ 11 | RUN bundle config set deployment 'true' 12 | RUN bundle install 13 | CMD ["bundle", "exec", "rspec"] 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ondřej Moravčík 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redmine installer 2 | 3 | > [!WARNING] 4 | > :exclamation: THIS TOOL IS NO LONGER MAINTAINED. 5 | > Easy Software Ltd. has stopped maintaining this tool. We are switching to dockerized installation and upgrade process. Please use our docker images for installation. 6 | > 7 | > Feel free to contact our support if you have any questions. 8 | 9 | Easy way hot to install/upgrade Redmine, Easy Redmine or Easy Project. 10 | 11 | Please do not run installer on background. It may happen that process will be paused by some event. For example database may require enter password during backuping database. 12 | 13 | > ⚠️ Current version support only **ruby 3.1.2**, **Percona 8** as DB server and **redmine 5.0.3+** 14 | 15 | Version **v3.x** is designed primarily for work with Easy Redmine / Easy Project **v12.x**. 16 | 17 | ## Installation 18 | 19 | Install tool into your server environment by: 20 | ```bash 21 | gem install redmine-installer 22 | ``` 23 | 24 | > ⚠️ Version `v3.0.0` and higher require ruby 3.1.2 and its designed for Easy Redmine / Easy Project v12 and Redmine 5.0.3+ respectively. 25 | ## Examples 26 | 27 | To display global documentation for installer. 28 | 29 | ```bash 30 | redmine help 31 | ``` 32 | 33 | You can also check more detailed documentation for each command. 34 | 35 | ```bash 36 | redmine help [COMMAND] 37 | ``` 38 | 39 | ### Environment checker 40 | Check your server environment if it meets our requirements for best Easy Redmine experience. 41 | ```bash 42 | redmine env_check 43 | ``` 44 | 45 | ### Installing 46 | 47 | Create new project on empty directory. All argument are optional. Directory should be empty because installer delete all content for ensuring correct installation. If directory does not exist, current user must have privileges to create it. 48 | 49 | ```bash 50 | redmine help install 51 | redmine install [PACKAGE PATH or URL] [REDMINE_ROOT] [options] 52 | ``` 53 | 54 | ``` 55 | --bundle-options OPTIONS Options for bundle install 56 | --silent Less verbose installation 57 | ``` 58 | 59 | Examples: 60 | 61 | Install Redmine. Installer will ask for every required parameters. 62 | - `redmine install` 63 | 64 | Install Redmine from internet 65 | - `redmine install https://server.tld/REDMINE_PACKAGE.zip /srv/redmine` 66 | 67 | Install Redmine from redmine.zip package into /srv/redmine folder. 68 | - `redmine install redmine.zip /srv/redmine` 69 | 70 | Install Redmine without rmgaick dependencies. 71 | - `redmine install redmine.zip /srv/redmine --bundle-options "--without rmagick"` 72 | 73 | ### Upgrading 74 | 75 | Upgrade existing project with new package. Full and correct upgrading is ensured by these steps: 76 | 1. project is build on temporary directory 77 | 2. previous root is deleted 78 | 3. projects is moved to target location 79 | 80 | Since current root is deleted you should use option `--keep` if you want preserved some files. 81 | 82 | 83 | ``` 84 | redmine help upgrade 85 | redmine upgrade [PACKAGE PATH or URL] [REDMINE_ROOT] [options] 86 | ``` 87 | 88 | ``` 89 | --bundle-options OPTIONS Options for bundle install 90 | --silent Less verbose upgrading 91 | --profile PROFILE_ID Using saved profile 92 | --keep PATH(s) Keep selected files or directories 93 | ``` 94 | 95 | Examples: 96 | 97 | Upgrade Redmine located on /srv/redmine with package redmine2.zip 98 | - `redmine upgrade redmine2.zip /srv/redmine` 99 | 100 | Upgrade Redmine from internet 101 | - `redmine upgrade https://server.tld/REDMINE_PACKAGE.zip /srv/redmine` 102 | 103 | Upgrade Redmine and keep directory. 104 | - `redmine upgrade redmine2.zip /srv/redmine --keep directory_i_want_keep` 105 | 106 | Once you've saved profile you can use previouse "answer" again. 107 | - `redmine upgrade redmine2.zip /srv/redmine --profile 1` 108 | 109 | ### Backuping 110 | 111 | Backup existing project. You can backup full redmine with database or just database. 112 | 113 | ``` 114 | redmine help backup 115 | redmine backup [REDMINE_ROOT] 116 | ``` 117 | 118 | Examples: 119 | 120 | Backup project located in /srv/redmine 121 | - `redmine upgrade /srv/redmine` 122 | 123 | ### Restoring database 124 | 125 | Restore database dump to redmine. 126 | 127 | ``` 128 | redmine help restore-db 129 | redmine restore-db DATABASE_DUMP [REDMINE_ROOT] [options] 130 | ``` 131 | 132 | Examples: 133 | 134 | Restore database db.dump for redmine in /srv/redmine 135 | - `redmine restore-db db.dump /srv/redmine` 136 | 137 | ## Testing 138 | 139 | Just run: 140 | ```bash 141 | docker compose up --build --exit-code-from redmine_installer 142 | ``` 143 | This will build docker image from current code and run tests against _percona8_. 144 | 145 | note: Require Docker of course. 146 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new 5 | 6 | task default: :spec 7 | task test: :spec 8 | -------------------------------------------------------------------------------- /bin/redmine: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../gems.rb', __dir__) 4 | 5 | require 'bundler' 6 | Bundler.require(:default) 7 | 8 | lib = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 9 | $LOAD_PATH.unshift(lib) if !$LOAD_PATH.include?(lib) 10 | 11 | require 'redmine-installer' 12 | 13 | RedmineInstaller.print_logo 14 | RedmineInstaller::CLI.new.run 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: percona:8 4 | ports: 5 | - "3306" 6 | cap_add: 7 | - SYS_NICE # CAP_SYS_NICE 8 | environment: 9 | MYSQL_RANDOM_ROOT_PASSWORD: root 10 | MYSQL_DATABASE: easy 11 | MYSQL_PASSWORD: easy 12 | MYSQL_USER: easy 13 | redmine_installer: 14 | build: 15 | context: . 16 | environment: 17 | DB_HOST: db 18 | MYSQL_PASSWORD: easy 19 | MYSQL_USER: easy 20 | MYSQL_DATABASE: easy 21 | volumes: 22 | - ./:/gem 23 | working_dir: /gem 24 | depends_on: 25 | - db 26 | command: ["/bin/bash", "-l", "-c", "bundle install && bundle exec rspec"] 27 | -------------------------------------------------------------------------------- /gems.locked: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | redmine-installer (3.0.4) 5 | bundler (>= 2.3.7) 6 | commander 7 | pastel (~> 0.8.0) 8 | rubyzip 9 | tty-progressbar (~> 0.18.2) 10 | tty-prompt (~> 0.23.1) 11 | tty-spinner (~> 0.8.0) 12 | 13 | GEM 14 | remote: https://rubygems.org/ 15 | specs: 16 | childprocess (4.1.0) 17 | coderay (1.1.3) 18 | commander (5.0.0) 19 | highline (~> 3.0.0) 20 | diff-lcs (1.5.0) 21 | highline (3.0.1) 22 | method_source (1.0.0) 23 | pastel (0.8.0) 24 | tty-color (~> 0.5) 25 | pry (0.14.1) 26 | coderay (~> 1.1) 27 | method_source (~> 1.0) 28 | rake (13.0.6) 29 | rspec (3.11.0) 30 | rspec-core (~> 3.11.0) 31 | rspec-expectations (~> 3.11.0) 32 | rspec-mocks (~> 3.11.0) 33 | rspec-core (3.11.0) 34 | rspec-support (~> 3.11.0) 35 | rspec-expectations (3.11.1) 36 | diff-lcs (>= 1.2.0, < 2.0) 37 | rspec-support (~> 3.11.0) 38 | rspec-mocks (3.11.2) 39 | diff-lcs (>= 1.2.0, < 2.0) 40 | rspec-support (~> 3.11.0) 41 | rspec-support (3.11.1) 42 | rubyzip (2.3.2) 43 | strings-ansi (0.2.0) 44 | tty-color (0.6.0) 45 | tty-cursor (0.7.1) 46 | tty-progressbar (0.18.2) 47 | strings-ansi (~> 0.2) 48 | tty-cursor (~> 0.7) 49 | tty-screen (~> 0.8) 50 | unicode-display_width (>= 1.6, < 3.0) 51 | tty-prompt (0.23.1) 52 | pastel (~> 0.8) 53 | tty-reader (~> 0.8) 54 | tty-reader (0.9.0) 55 | tty-cursor (~> 0.7) 56 | tty-screen (~> 0.8) 57 | wisper (~> 2.0) 58 | tty-screen (0.8.2) 59 | tty-spinner (0.8.0) 60 | tty-cursor (>= 0.5.0) 61 | unicode-display_width (2.5.0) 62 | wisper (2.0.1) 63 | 64 | PLATFORMS 65 | arm64-darwin-23 66 | x86_64-darwin-21 67 | x86_64-linux 68 | 69 | DEPENDENCIES 70 | childprocess (~> 4.1.0) 71 | pry 72 | rake 73 | redmine-installer! 74 | rspec (~> 3.11.0) 75 | 76 | BUNDLED WITH 77 | 2.5.5 78 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in redmine-installer.gemspec 4 | gemspec 5 | 6 | # if File.exist?(File.join(__dir__, '.local')) 7 | # group :development, :test do 8 | # gem 'pry' 9 | # gem 'rspec' 10 | # gem 'childprocess' 11 | # end 12 | # end 13 | gem "pry", group: [:development, :test] 14 | -------------------------------------------------------------------------------- /lib/redmine-installer.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'tempfile' 3 | require 'bundler' 4 | require 'ostruct' 5 | require 'tmpdir' 6 | require 'pastel' 7 | require 'yaml' 8 | require 'zip' 9 | 10 | require 'tty-progressbar' 11 | require 'tty-spinner' 12 | require 'tty-prompt' 13 | 14 | module RedmineInstaller 15 | autoload :CLI, 'redmine-installer/cli' 16 | autoload :Task, 'redmine-installer/task' 17 | autoload :Install, 'redmine-installer/install' 18 | autoload :Utils, 'redmine-installer/utils' 19 | autoload :Logger, 'redmine-installer/logger' 20 | autoload :TaskModule, 'redmine-installer/task_module' 21 | autoload :Environment, 'redmine-installer/environment' 22 | autoload :Redmine, 'redmine-installer/redmine' 23 | autoload :Package, 'redmine-installer/package' 24 | autoload :Database, 'redmine-installer/database' 25 | autoload :Configuration, 'redmine-installer/configuration' 26 | autoload :Upgrade, 'redmine-installer/upgrade' 27 | autoload :Command, 'redmine-installer/command' 28 | autoload :Profile, 'redmine-installer/profile' 29 | autoload :Backup, 'redmine-installer/backup' 30 | autoload :RestoreDB, 'redmine-installer/restore_db' 31 | autoload :PackageConfig, 'redmine-installer/package_config' 32 | autoload :Easycheck, 'redmine-installer/easycheck' 33 | 34 | # Settings 35 | MIN_SUPPORTED_RUBY = '3.1.2' 36 | 37 | def self.logger 38 | @logger ||= RedmineInstaller::Logger.new 39 | end 40 | 41 | def self.prompt 42 | @prompt ||= TTY::Prompt.new 43 | end 44 | 45 | def self.pastel 46 | @pastel ||= Pastel.new 47 | end 48 | 49 | def self.print_logo 50 | puts <<-PRINT 51 | __ _ 52 | _______ ___/ /_ _ (_)__ ___ 53 | / __/ -_) _ / ' \\/ / _ \\/ -_) 54 | /_/ \\__/\\_,_/_/_/_/_/_//_/\\__/ 55 | 56 | 57 | Powered by EasySoftware 58 | 59 | PRINT 60 | end 61 | end 62 | 63 | 64 | # Requirements 65 | require 'redmine-installer/version' 66 | require 'redmine-installer/errors' 67 | 68 | # Patches 69 | require 'redmine-installer/patches/ruby' 70 | require 'redmine-installer/patches/tty' 71 | 72 | if ENV['REDMINE_INSTALLER_SPEC'] 73 | require 'redmine-installer/spec/spec' 74 | end 75 | 76 | Kernel.at_exit do 77 | RedmineInstaller.logger.finish 78 | end 79 | -------------------------------------------------------------------------------- /lib/redmine-installer/backup.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | class Backup < Task 3 | 4 | def initialize(redmine_root) 5 | super() 6 | @target_redmine = Redmine.new(self, redmine_root) 7 | end 8 | 9 | def up 10 | @target_redmine.ensure_and_valid_root 11 | @target_redmine.validate 12 | @target_redmine.check_running_state 13 | @target_redmine.make_backup 14 | 15 | puts 16 | puts pastel.bold('Redmine was backuped') 17 | logger.info('Redmine was backuped') 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/redmine-installer/cli.rb: -------------------------------------------------------------------------------- 1 | require 'commander' 2 | 3 | module Commander 4 | module UI 5 | # Disable paging for 'classic' help 6 | def self.enable_paging 7 | end 8 | end 9 | end 10 | 11 | module RedmineInstaller 12 | class CLI 13 | include Commander::Methods 14 | 15 | def run 16 | program :name, 'Ruby installer' 17 | program :version, RedmineInstaller::VERSION 18 | program :description, 'Easy way how install/upgrade redmine or plugin.' 19 | 20 | global_option('-d', '--debug', 'Logging message to stdout') { $DEBUG = true } 21 | global_option('-s', '--silent', 'Be less version in output') { $SILENT_MODE = true } 22 | global_option('-e', '--env', 'For backward compatibility. Production is now always use.') 23 | global_option('--run-easycheck', 'Run easycheck.sh from https://github.com/easyredmine/easy_server_requirements_check') do 24 | RedmineInstaller::Easycheck.run 25 | end 26 | 27 | default_command :help 28 | 29 | # --- Environment checker ----------------------------------------------- 30 | # This command is very specify to Easy Redmine 31 | command :env_check do |c| 32 | c.syntax = 'env_check' 33 | c.description = <<~DESC 34 | Check current environment if it meet requirements for Easy Redmine / Easy Project. 35 | 36 | See: https://www.easyredmine.com/documentation-of-easy-redmine/article/hardware-and-software-requirements-for-the-server-solution 37 | DESC 38 | c.action do |_args| 39 | task = RedmineInstaller::Task.new 40 | RedmineInstaller::Environment.new(task).check 41 | puts task.pastel.green "OK" 42 | rescue StandardError => e 43 | puts task.pastel.red e.message 44 | 45 | puts "\n\nCheck: https://www.easyredmine.com/documentation-of-easy-redmine/article/hardware-and-software-requirements-for-the-server-solution#Software%20requirements for more info." 46 | end 47 | end 48 | 49 | alias_command :check, :env_check 50 | 51 | # --- Install ----------------------------------------------------------- 52 | command :install do |c| 53 | c.syntax = 'install [PACKAGE] [REDMINE_ROOT] [options]' 54 | c.description = 'Install redmine or easyredmine' 55 | 56 | c.example 'Install from archive', 57 | 'redmine install ~/REDMINE_PACKAGE.zip' 58 | c.example 'Install package from internet', 59 | 'redmine install https://server.tld/REDMINE_PACKAGE.zip' 60 | c.example 'Install specific version from internet', 61 | 'redmine install v3.1.0' 62 | c.example 'Install package to new dir', 63 | 'redmine install ~/REDMINE_PACKAGE.zip redmine_root' 64 | 65 | c.option '--enable-user-root', 'Skip root as root validation' 66 | c.option '--bundle-options OPTIONS', String, 'Add options to bundle command' 67 | c.option '--database-dump DUMP', String, 'Load dump before migration (experimental function)' 68 | 69 | c.action do |args, options| 70 | options.default(enable_user_root: false) 71 | 72 | RedmineInstaller::Install.new(args[0], args[1], **options.__hash__).run 73 | end 74 | end 75 | alias_command :i, :install 76 | 77 | # --- Upgrade ----------------------------------------------------------- 78 | command :upgrade do |c| 79 | c.syntax = 'upgrade [PACKAGE] [REDMINE_ROOT] [options]' 80 | c.description = 'Upgrade redmine or easyredmine' 81 | 82 | c.example 'Upgrade with new package', 83 | 'redmine upgrade ~/REDMINE_PACKAGE.zip' 84 | c.example 'Upgrade with package from internet', 85 | 'redmine upgrade ~/REDMINE_PACKAGE.zip redmine_root' 86 | c.example 'Upgrade', 87 | 'redmine upgrade https://server.tld/REDMINE_PACKAGE.zip redmine_root' 88 | c.example 'Upgrade and keep directory', 89 | 'redmine upgrade --keep git_repositories' 90 | 91 | c.option '--enable-user-root', 'Skip root as root validation' 92 | c.option '--bundle-options OPTIONS', String, 'Add options to bundle command' 93 | c.option '-p', '--profile PROFILE_ID', Integer, 'Use saved profile' 94 | c.option '--keep PATH(s)', Array, 'Keep paths, use multiple options or separate values by comma (paths must be relative)', &method(:parse_keep_options) 95 | c.option '--copy-files-with-symlink', 'Files will be referenced by symlinks instead of copying files. Only for advance users.' 96 | 97 | c.action do |args, options| 98 | options.default(enable_user_root: false, copy_files_with_symlink: false) 99 | 100 | RedmineInstaller::Upgrade.new(args[0], args[1], **options.__hash__).run 101 | end 102 | end 103 | alias_command :u, :upgrade 104 | 105 | # --- Verify log -------------------------------------------------------- 106 | command :'verify-log' do |c| 107 | c.syntax = 'verify-log LOGFILE' 108 | c.description = 'Verify redmine installer log file' 109 | 110 | c.example 'Verify log', 111 | 'redmine verify-log LOGFILE' 112 | 113 | c.action do |args, _| 114 | RedmineInstaller::Logger.verify(args[0]) 115 | end 116 | end 117 | 118 | # --- Backup ------------------------------------------------------------ 119 | command :backup do |c| 120 | c.syntax = 'backup [REDMINE_ROOT]' 121 | c.description = 'Backup redmine' 122 | 123 | c.example 'Backup', 124 | 'redmine backup /srv/redmine' 125 | 126 | c.option '--enable-user-root', 'Skip root as root validation' # just for consistency 127 | 128 | c.action do |args, options| 129 | options.default(enable_user_root: false) 130 | 131 | RedmineInstaller::Backup.new(args[0]).run 132 | end 133 | end 134 | alias_command :b, :backup 135 | 136 | # --- Restore db -------------------------------------------------------- 137 | command :'restore-db' do |c| 138 | c.syntax = 'restore-db DATABASE_DUMP [REDMINE_ROOT] [options]' 139 | c.description = 'Restore database and delete old data' 140 | 141 | c.example 'Restore DB', 142 | 'redmine restore-db /srv/redmine.sql' 143 | 144 | c.option '--enable-user-root', 'Skip root as root validation' 145 | 146 | c.action do |args, options| 147 | options.default(enable_user_root: false) 148 | 149 | RedmineInstaller::RestoreDB.new(args[0], args[1]).run 150 | end 151 | end 152 | 153 | run! 154 | end 155 | 156 | # For multiple user --keep option 157 | def parse_keep_options(values) 158 | proxy_options = Commander::Runner.instance.active_command.proxy_options 159 | 160 | saved = proxy_options.find { |switch, _| switch == :keep } 161 | if saved 162 | saved[1].concat(values) 163 | else 164 | proxy_options << [:keep, values] 165 | end 166 | end 167 | 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /lib/redmine-installer/command.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module RedmineInstaller 4 | class Command 5 | include Utils 6 | 7 | attr_reader :cmd, :title, :formatter 8 | 9 | def initialize(cmd, title: nil) 10 | @cmd = cmd 11 | @title = title || cmd 12 | @formatter = $SILENT_MODE ? SilentFormatter.new : FullFormatter.new 13 | end 14 | 15 | def run 16 | Bundler.with_unbundled_env do 17 | run! 18 | end 19 | end 20 | 21 | def run! 22 | success = false 23 | 24 | logger.std("--> #{cmd}") 25 | 26 | formatter.print_title(title) 27 | 28 | status = Open3.popen2e(cmd) do |input, output, wait_thr| 29 | input.close 30 | 31 | output.each_line do |line| 32 | logger.std(line) 33 | formatter.print_line(line) 34 | end 35 | 36 | wait_thr.value 37 | end 38 | 39 | success = status.success? 40 | rescue => e 41 | success = false 42 | ensure 43 | formatter.print_end(success) 44 | end 45 | 46 | 47 | class BaseFormatter 48 | include Utils 49 | end 50 | 51 | class FullFormatter < BaseFormatter 52 | 53 | def print_title(title) 54 | puts '-->' 55 | puts "--> #{pastel.yellow(title)}" 56 | puts '-->' 57 | end 58 | 59 | def print_line(line) 60 | puts line 61 | end 62 | 63 | def print_end(*) 64 | end 65 | 66 | end 67 | 68 | class SilentFormatter < BaseFormatter 69 | 70 | def initialize 71 | @output = '' 72 | end 73 | 74 | def print_title(title) 75 | format = "[#{pastel.yellow(':spinner')}] #{title}" 76 | @spinner = TTY::Spinner.new(format, success_mark: pastel.green('✔'), error_mark: pastel.red('✖')) 77 | @spinner.auto_spin 78 | end 79 | 80 | def print_line(line) 81 | @output << line 82 | end 83 | 84 | def print_end(success) 85 | if success 86 | @spinner.success 87 | else 88 | @spinner.error 89 | puts @output 90 | end 91 | end 92 | 93 | end 94 | 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/redmine-installer/configuration.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | ## 3 | # RedmineInstaller::Configuration 4 | # 5 | # For now, email is only configured. 6 | # 7 | class Configuration 8 | extend Utils 9 | 10 | def self.create_config(redmine) 11 | # Maybe: enum_select 12 | klass = prompt.select('Which service to use for email sending?') do |menu| 13 | menu.default 4 14 | 15 | menu.choice 'Custom configuration (SMTP)', CustomConfiguration 16 | menu.choice 'Gmail', Gmail 17 | menu.choice 'SendMail', SendMail 18 | menu.choice 'Nothing', Nothing 19 | end 20 | 21 | # Get parameters and create configuration 22 | database = klass.new(redmine) 23 | database.get_parameters 24 | database.make_config 25 | database 26 | end 27 | 28 | class Base 29 | include RedmineInstaller::Utils 30 | 31 | def initialize(redmine) 32 | @redmine = redmine 33 | end 34 | 35 | def get_parameters 36 | @user_name = prompt.ask('Username:', required: true) 37 | @password = prompt.mask('Password:', required: true) 38 | end 39 | 40 | def make_config 41 | File.open(@redmine.configuration_yml_path, 'w') do |f| 42 | f.puts(YAML.dump(build)) 43 | end 44 | end 45 | 46 | def build 47 | { 48 | 'default' => { 49 | 'email_delivery' => { 50 | 'delivery_method' => delivery_method, 51 | "#{delivery_method}_settings" => delivery_settings 52 | } 53 | } 54 | } 55 | end 56 | 57 | def delivery_method 58 | :smtp 59 | end 60 | 61 | def delivery_settings 62 | settings = {} 63 | 64 | # Required 65 | settings['address'] = @address 66 | settings['port'] = @port 67 | 68 | # Optional 69 | settings['authentication'] = @authentication.to_sym unless @authentication.to_s.empty? 70 | settings['domain'] = @domain unless @domain.to_s.empty? 71 | settings['user_name'] = @user_name unless @user_name.to_s.empty? 72 | settings['password'] = @password unless @password.to_s.empty? 73 | settings['tls'] = @enable_tls unless @enable_tls.to_s.empty? 74 | settings['enable_starttls_auto'] = @enable_starttls unless @enable_starttls.to_s.empty? 75 | settings['openssl_verify_mode'] = @openssl_verify unless @openssl_verify.to_s.empty? 76 | 77 | settings 78 | end 79 | 80 | def to_s 81 | "<#{class_name} #{@user_name}@#{@address}:#{@port}>" 82 | end 83 | 84 | end 85 | 86 | class Nothing < Base 87 | 88 | def get_parameters(*) end 89 | def make_config(*) end 90 | 91 | def to_s(*) 92 | "" 93 | end 94 | 95 | end 96 | 97 | class Gmail < Base 98 | 99 | def get_parameters 100 | super 101 | @address = 'smtp.gmail.com' 102 | @port = 587 103 | @domain = 'smtp.gmail.com' 104 | @authentication = :plain 105 | @enable_starttls = true 106 | end 107 | 108 | end 109 | 110 | class CustomConfiguration < Base 111 | 112 | def get_parameters 113 | super 114 | @address = prompt.ask('Address:', required: true) 115 | @port = prompt.ask('Port:', convert: lambda(&:to_i), required: true) 116 | @domain = prompt.ask('Domain:') 117 | @authentication = prompt.ask('Authentication:') 118 | @openssl_verify = prompt.ask('Openssl verify mode:') 119 | @enable_tls = prompt.yes?('Enable tls?:', default: false) 120 | @enable_starttls = prompt.yes?('Enable starttls?:', default: true) 121 | end 122 | 123 | end 124 | 125 | class SendMail < Base 126 | 127 | def get_parameters 128 | @location = prompt.ask('Location:', default: '/usr/sbin/sendmail', required: true) 129 | @arguments = prompt.ask('Arguments:', default: '-i -t') 130 | end 131 | 132 | def delivery_method 133 | :sendmail 134 | end 135 | 136 | def delivery_settings 137 | { 138 | 'location' => @location, 139 | 'arguments' => @arguments 140 | } 141 | end 142 | 143 | def to_s 144 | "" 145 | end 146 | 147 | end 148 | 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/redmine-installer/database.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | class Database 3 | extend Utils 4 | 5 | def self.create_config(redmine) 6 | # Maybe: enum_select 7 | klass = prompt.select('What database do you want use?') do |menu| 8 | menu.choice 'MySQL', MySQL 9 | menu.choice 'PostgreSQL', PostgreSQL 10 | end 11 | 12 | # Get parameters and create configuration 13 | database = klass.new(redmine) 14 | database.get_parameters 15 | database.make_config 16 | database 17 | end 18 | 19 | def self.init(redmine) 20 | unless File.exist?(redmine.database_yml_path) 21 | error "Database configuration files does not exist on #{redmine.root}." 22 | end 23 | 24 | definitions = YAML.load_file(redmine.database_yml_path, aliases: true) 25 | definition = definitions['production'] 26 | 27 | unless definition.is_a?(Hash) 28 | error 'Unknow database definition' 29 | end 30 | 31 | case definition['adapter'] 32 | when 'mysql', 'mysql2' 33 | klass = MySQL 34 | when 'pg', 'postgresql' 35 | klass = PostgreSQL 36 | else 37 | error "Unknow database adapter #{definition['adapter']}." 38 | end 39 | 40 | database = klass.new(redmine) 41 | database.set_paramaters(definition) 42 | database 43 | end 44 | 45 | class Base 46 | include RedmineInstaller::Utils 47 | 48 | attr_reader :backup 49 | 50 | def initialize(redmine) 51 | @redmine = redmine 52 | end 53 | 54 | def backuped? 55 | @backup && File.exist?(@backup) 56 | end 57 | 58 | def get_parameters 59 | @database = prompt.ask('Database:', required: true) 60 | @host = prompt.ask('Host:', default: 'localhost', required: true) 61 | @username = prompt.ask('Username:', default: '') 62 | @password = prompt.mask('Password:', default: '') 63 | @encoding = prompt.ask('Encoding:', default: 'utf8mb4', required: true) 64 | @port = prompt.ask('Port:', default: default_port, convert: lambda(&:to_i), required: true) 65 | end 66 | 67 | def set_paramaters(definition) 68 | @database = definition['database'] 69 | @username = definition['username'] 70 | @password = definition['password'] 71 | @encoding = definition['encoding'] 72 | @host = definition['host'] 73 | @port = definition['port'] 74 | end 75 | 76 | def make_config 77 | File.open(@redmine.database_yml_path, 'w') do |f| 78 | f.puts(YAML.dump(build)) 79 | end 80 | end 81 | 82 | def make_backup(dir) 83 | puts 'Database backuping' 84 | @backup = File.join(dir, "#{@database}.sql") 85 | Kernel.system backup_command(@backup) 86 | end 87 | 88 | # Recreate database should be done in 2 commands because of 89 | # postgre's '--command' options which can do only 1 operations. 90 | # Otherwise result is unpredictable. 91 | def do_restore(file) 92 | puts 'Database cleaning' 93 | Kernel.system drop_database_command 94 | Kernel.system create_database_command 95 | 96 | puts 'Database restoring' 97 | Kernel.system restore_command(file) 98 | end 99 | 100 | def build 101 | data = {} 102 | data['adapter'] = adapter_name 103 | data['database'] = @database 104 | data['username'] = @username if @username.present? 105 | data['password'] = @password if @password.present? 106 | data['encoding'] = @encoding 107 | data['host'] = @host 108 | data['port'] = @port 109 | 110 | { 111 | 'production' => data, 112 | 'development' => data 113 | } 114 | end 115 | 116 | def to_s 117 | "<#{class_name} #{@username}@#{@host}:#{@port} (#{@encoding})>" 118 | end 119 | 120 | end 121 | 122 | class MySQL < Base 123 | 124 | def default_port 125 | 3306 126 | end 127 | 128 | def adapter_name 129 | 'mysql2' 130 | end 131 | 132 | def command_args 133 | args = [] 134 | args << "--host=#{@host}" unless @host.to_s.empty? 135 | args << "--port=#{@port}" unless @port.to_s.empty? 136 | args << "--user=#{@username}" unless @username.to_s.empty? 137 | args << "--password=#{@password}" unless @password.to_s.empty? 138 | args.join(' ') 139 | end 140 | 141 | def create_database_command 142 | "mysql #{command_args} --execute=\"create database #{@database}\"" 143 | end 144 | 145 | def drop_database_command 146 | "mysql #{command_args} --execute=\"drop database #{@database}\"" 147 | end 148 | 149 | def backup_command(file) 150 | "mysqldump --add-drop-database --compact --result-file=#{file} #{command_args} #{@database}" 151 | end 152 | 153 | def restore_command(file) 154 | "mysql #{command_args} #{@database} < #{file}" 155 | end 156 | 157 | end 158 | 159 | class PostgreSQL < Base 160 | 161 | def default_port 162 | 5432 163 | end 164 | 165 | def adapter_name 166 | 'postgresql' 167 | end 168 | 169 | def command_args 170 | args = [] 171 | args << "--host=#{@host}" unless @host.to_s.empty? 172 | args << "--port=#{@port}" unless @port.to_s.empty? 173 | args << "--username=#{@username}" unless @username.to_s.empty? 174 | args.join(' ') 175 | end 176 | 177 | def cli_password 178 | if @password.present? 179 | "PGPASSWORD=\"#{@password}\"" 180 | else 181 | '' 182 | end 183 | end 184 | 185 | def create_database_command 186 | "#{cli_password} psql #{command_args} --command=\"create database #{@database};\"" 187 | end 188 | 189 | def drop_database_command 190 | "#{cli_password} psql #{command_args} --command=\"drop database #{@database};\"" 191 | end 192 | 193 | def backup_command(file) 194 | "#{cli_password} pg_dump --clean #{command_args} --format=custom --file=#{file} #{@database}" 195 | end 196 | 197 | def restore_command(file) 198 | "#{cli_password} pg_restore --clean #{command_args} --dbname=#{@database} #{file} 2>/dev/null" 199 | end 200 | 201 | end 202 | 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /lib/redmine-installer/easycheck.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module RedmineInstaller 4 | class Easycheck 5 | extend Utils 6 | 7 | EASYCHECK_SH = 'https://raw.githubusercontent.com/easyredmine/easy_server_requirements_check/master/easycheck.sh' 8 | 9 | def self.run 10 | Bundler.with_unbundled_env do 11 | if Kernel.system('which', 'wget') 12 | Open3.pipeline(['wget', EASYCHECK_SH, '-O', '-', '--quiet'], 'bash') 13 | 14 | elsif Kernel.system('which', 'curl') 15 | Open3.pipeline(['curl', EASYCHECK_SH, '--output', '-', '--silent'], 'bash') 16 | 17 | else 18 | error 'Neither wget nor curl was found' 19 | end 20 | end 21 | 22 | puts 23 | if !prompt.yes?('Continue?') 24 | error 'Canceled' 25 | end 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/redmine-installer/environment.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | class Environment < TaskModule 3 | 4 | def check 5 | if user_is_root? && !task.options.enable_user_root 6 | error 'You cannot install redmine under root. Please change user.' 7 | end 8 | 9 | if Gem::Version.new(RUBY_VERSION) < Gem::Version.new(RedmineInstaller::MIN_SUPPORTED_RUBY) 10 | error "You are using unsupported ruby. Please install ruby v#{RedmineInstaller::MIN_SUPPORTED_RUBY}." 11 | end 12 | 13 | if mysql_version =~ /^mysql\s+Ver\s(8\.\d)/ 14 | unless mysql_version.include? "Percona" 15 | puts pastel.on_red 'WARNING: It seems you are using MySQL 8.x, but ER / EP supported only Percona 8 db.' 16 | end 17 | else 18 | error 'Only Percona 8.x db server is supported!.' 19 | end 20 | 21 | logger.info 'Environment checked' 22 | end 23 | 24 | def user_is_root? 25 | Process.euid == 0 26 | end 27 | 28 | def mysql_version 29 | `mysql --version` 30 | rescue StandardError 31 | "" 32 | end 33 | 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/redmine-installer/errors.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | class Error < StandardError 3 | end 4 | 5 | class ProfileError < Error 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/redmine-installer/install.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | class Install < Task 3 | 4 | attr_reader :package_config 5 | 6 | def initialize(package, redmine_root, **options) 7 | super(**options) 8 | 9 | @environment = Environment.new(self) 10 | @package = Package.new(self, package) 11 | @target_redmine = Redmine.new(self, redmine_root) 12 | @temp_redmine = Redmine.new(self) 13 | @package_config = PackageConfig.new(@temp_redmine) 14 | end 15 | 16 | def up 17 | @temp_redmine.valid_options 18 | @environment.check 19 | @target_redmine.ensure_and_valid_root 20 | 21 | @package.ensure_and_valid_package 22 | @package.extract 23 | 24 | @temp_redmine.root = @package.redmine_root 25 | @package_config.check_version 26 | @temp_redmine.validate 27 | 28 | @temp_redmine.create_database_yml 29 | @temp_redmine.create_configuration_yml 30 | @temp_redmine.install 31 | 32 | print_title('Finishing installation') 33 | ok('Cleaning root'){ @target_redmine.delete_root } 34 | ok('Moving redmine to target directory'){ @target_redmine.move_from(@temp_redmine) } 35 | ok('Cleanning up'){ @package.clean_up } 36 | ok('Moving installer log'){ logger.move_to(@target_redmine, suffix: 'install') } 37 | 38 | puts 39 | puts pastel.bold('Redmine was installed') 40 | logger.info('Redmine was installed') 41 | end 42 | 43 | def down 44 | @temp_redmine.clean_up 45 | @package.clean_up 46 | 47 | puts 48 | puts "(Log is located on #{pastel.bold(logger.path)})" 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/redmine-installer/logger.rb: -------------------------------------------------------------------------------- 1 | require 'digest' 2 | 3 | module RedmineInstaller 4 | class Logger 5 | 6 | def self.verify(log_file) 7 | log_file = log_file.to_s 8 | 9 | unless File.exist?(log_file) 10 | puts "File '#{log_file}' does not exist." 11 | exit(false) 12 | end 13 | 14 | content = File.open(log_file, &:to_a) 15 | digest1 = content.pop 16 | digest2 = Digest::SHA256.hexdigest(content.join) 17 | 18 | if digest1 == digest2 19 | puts RedmineInstaller.pastel.green("Logfile is OK. Digest verified.") 20 | else 21 | puts RedmineInstaller.pastel.red("Logfile is not OK. Digest wasn't verified.") 22 | end 23 | end 24 | 25 | def initialize 26 | if ENV['REDMINE_INSTALLER_LOGFILE'] 27 | @output = File.open(ENV['REDMINE_INSTALLER_LOGFILE'], 'w') 28 | else 29 | @output = Tempfile.create('redmine_installer.log') 30 | end 31 | end 32 | 33 | def path 34 | @output.path 35 | end 36 | 37 | def finish 38 | close 39 | digest = Digest::SHA256.file(path).hexdigest 40 | File.open(path, 'a') { |f| f.write(digest) } 41 | end 42 | 43 | def close 44 | @output.flush 45 | @output.close 46 | end 47 | 48 | def move_to(redmine, suffix: '%d%m%Y_%H%M%S') 49 | close 50 | 51 | new_path = File.join(redmine.log_path, Time.now.strftime("redmine_installer_#{suffix}.log")) 52 | 53 | FileUtils.mkdir_p(redmine.log_path) 54 | FileUtils.mv(path, new_path) 55 | @output = File.open(new_path, 'a+') 56 | end 57 | 58 | def info(*messages) 59 | log(' INFO', *messages) 60 | end 61 | 62 | def error(*messages) 63 | log('ERROR', *messages) 64 | end 65 | 66 | def warn(*messages) 67 | log(' WARN', *messages) 68 | end 69 | 70 | def std(*messages) 71 | log(' STD', *messages) 72 | end 73 | 74 | def log(severity, *messages) 75 | messages.each do |message| 76 | @output.puts("#{severity}: #{message}") 77 | end 78 | 79 | @output.flush 80 | end 81 | 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/redmine-installer/package.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems/package' 2 | require 'net/http' 3 | require 'zlib' 4 | require 'uri' 5 | 6 | module RedmineInstaller 7 | class Package < TaskModule 8 | 9 | SUPPORTED_ARCHIVE_FORMATS = ['.zip', '.gz', '.tgz'] 10 | TAR_LONGLINK = '././@LongLink' 11 | 12 | attr_reader :package, :temp_dir 13 | 14 | def initialize(task, package) 15 | super(task) 16 | @package = package.to_s 17 | end 18 | 19 | def ensure_and_valid_package 20 | if package.empty? 21 | @package = prompt.ask('Path to package:', required: true) 22 | end 23 | 24 | if !File.exist?(@package) 25 | if @package =~ /\Av?(\d\.\d\.\d)\Z/ 26 | @package = download_redmine_version($1) 27 | elsif valid_url?(@package) 28 | @package = download_from_url(@package) 29 | else 30 | error "File doesn't exist #{@package}" 31 | end 32 | end 33 | 34 | @type = File.extname(@package) 35 | unless SUPPORTED_ARCHIVE_FORMATS.include?(@type) 36 | error "File #{@package} must have format: #{SUPPORTED_ARCHIVE_FORMATS.join(', ')}" 37 | end 38 | end 39 | 40 | def extract 41 | print_title('Extracting redmine package') 42 | 43 | @temp_dir = Dir.mktmpdir 44 | 45 | case @type 46 | when '.zip' 47 | extract_zip 48 | when '.gz', '.tgz' 49 | extract_tar_gz 50 | end 51 | 52 | logger.info("Package was loaded into #{@temp_dir}.") 53 | end 54 | 55 | # Move files from temp dir to target. First check if folder contains redmine 56 | # or contains folder which contains redmine. 57 | # 58 | # Package can have format: 59 | # |-- redmine-2 60 | # |-- app 61 | # `-- config 62 | # ... 63 | # 64 | def redmine_root 65 | root = @temp_dir 66 | 67 | loop { 68 | ls = Dir.glob(File.join(root, '*')) 69 | 70 | if ls.size == 1 71 | root = ls.first 72 | else 73 | break 74 | end 75 | } 76 | 77 | root 78 | end 79 | 80 | def clean_up 81 | @temp_dir && FileUtils.remove_entry_secure(@temp_dir) 82 | @temp_file && FileUtils.remove_entry_secure(@temp_file) 83 | end 84 | 85 | private 86 | 87 | def valid_url?(string) 88 | uri = URI.parse(string) 89 | %w[http https].include?(uri.scheme) 90 | rescue URI::BadURIError, URI::InvalidURIError 91 | false 92 | end 93 | 94 | def download_redmine_version(version) 95 | url = "https://www.redmine.org/releases/redmine-#{version}.zip" 96 | download_from_url(url) 97 | end 98 | 99 | def download_from_url(url) 100 | uri = URI.parse(url) 101 | 102 | @temp_file = Tempfile.new(%w[redmine .zip]) 103 | @temp_file.binmode 104 | 105 | Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| 106 | head = http.request_head(uri) 107 | 108 | unless head.is_a?(Net::HTTPSuccess) 109 | error "Cannot download #{uri}" 110 | end 111 | 112 | print_title("Downloading #{uri}") 113 | progressbar = TTY::ProgressBar.new(PROGRESSBAR_FORMAT, total: head['content-length'].to_i, frequency: 2, clear: true) 114 | 115 | http.get(uri) do |data| 116 | @temp_file.write(data) 117 | progressbar.advance(data.size) 118 | end 119 | 120 | progressbar.finish 121 | end 122 | 123 | logger.info("#{uri} downloaded") 124 | 125 | @temp_file.close 126 | @temp_file.path 127 | end 128 | 129 | def extract_zip 130 | Zip::File.open(@package) do |zip_file| 131 | # Progressbar 132 | progressbar = TTY::ProgressBar.new(PROGRESSBAR_FORMAT, total: zip_file.size, frequency: 2, clear: true) 133 | 134 | zip_file.each do |entry| 135 | dest_file = File.join(@temp_dir, entry.name) 136 | FileUtils.mkdir_p(File.dirname(dest_file)) 137 | 138 | entry.extract(dest_file) 139 | progressbar.advance(1) 140 | end 141 | 142 | progressbar.finish 143 | end 144 | 145 | end 146 | 147 | # Extract .tar.gz archive 148 | # based on http://dracoater.blogspot.cz/2013/10/extracting-files-from-targz-with-ruby.html 149 | # 150 | # Originally tar did not support paths longer than 100 chars. GNU tar is better and they 151 | # implemented support for longer paths, but it was made through a hack called ././@LongLink. 152 | # Shortly speaking, if you stumble upon an entry in tar archive which path equals to above 153 | # mentioned ././@LongLink, that means that the following entry path is longer than 100 chars and 154 | # is truncated. The full path of the following entry is actually the value of the current entry. 155 | # 156 | def extract_tar_gz 157 | Gem::Package::TarReader.new(Zlib::GzipReader.open(@package)) do |tar| 158 | 159 | # Progressbar 160 | progressbar = TTY::ProgressBar.new(PROGRESSBAR_FORMAT, total: tar.count, frequency: 2, clear: true) 161 | 162 | # tar.count move position pointer to end 163 | tar.rewind 164 | 165 | dest_file = nil 166 | tar.each do |entry| 167 | 168 | if entry.full_name == TAR_LONGLINK 169 | dest_file = File.join(@temp_dir, entry.read.strip) 170 | next 171 | end 172 | 173 | # Pax header 174 | # "%d %s=%s\n", , , 175 | if entry.header.typeflag == 'x' 176 | 177 | pax_headers = entry.read.split("\n") 178 | pax_headers.each do |header| 179 | meta, value = header.split('=', 2) 180 | length, keyword = meta.split(' ', 2) 181 | 182 | if keyword == 'path' 183 | dest_file = File.join(@temp_dir, value) 184 | next 185 | end 186 | end 187 | 188 | # If there is no header with keyword "path" 189 | next 190 | end 191 | 192 | dest_file ||= File.join(@temp_dir, entry.full_name) 193 | if entry.directory? 194 | FileUtils.rm_rf(dest_file) unless File.directory?(dest_file) 195 | FileUtils.mkdir_p(dest_file, mode: entry.header.mode, verbose: false) 196 | elsif entry.file? 197 | FileUtils.rm_rf(dest_file) unless File.file?(dest_file) 198 | File.open(dest_file, 'wb') do |f| 199 | f.write(entry.read) 200 | end 201 | FileUtils.chmod(entry.header.mode, dest_file, verbose: false) 202 | elsif entry.header.typeflag == '2' # symlink 203 | File.symlink(entry.header.linkname, dest_file) 204 | end 205 | 206 | dest_file = nil 207 | progressbar.advance(1) 208 | end 209 | 210 | progressbar.finish 211 | end 212 | end 213 | 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /lib/redmine-installer/package_config.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | class PackageConfig 3 | include Utils 4 | 5 | CONFIG_DIR = '_package' 6 | 7 | def initialize(redmine) 8 | @redmine = redmine 9 | end 10 | 11 | def min_version 12 | options['min_version'] 13 | end 14 | 15 | def dump_type 16 | options['dump_type'] 17 | end 18 | 19 | def dump_file 20 | File.join(@redmine.root, CONFIG_DIR, options['dump_file'].to_s) 21 | end 22 | 23 | def dump_attached? 24 | File.exist?(dump_file) 25 | end 26 | 27 | def sql_dump_file 28 | if defined?(@sql_dump_file) 29 | return @sql_dump_file 30 | end 31 | 32 | if !dump_attached? 33 | @sql_dump_file = nil 34 | end 35 | 36 | if dump_file.end_with?('.gz') 37 | @sql_dump_file = File.join(@redmine.root, CONFIG_DIR, 'dump.sql') 38 | 39 | Zlib::GzipReader.open(dump_file) { |gz| 40 | File.binwrite(@sql_dump_file, gz.read) 41 | } 42 | else 43 | @sql_dump_file = dump_file 44 | end 45 | 46 | @sql_dump_file 47 | end 48 | 49 | def dump_compatible?(database) 50 | database.adapter_name.start_with?(dump_type.to_s) 51 | end 52 | 53 | def options 54 | @options ||= _options 55 | end 56 | 57 | def check_version 58 | if min_version && Gem::Version.new(min_version) > Gem::Version.new(RedmineInstaller::VERSION) 59 | error "You are using an old version of installer. Min version is #{min_version} (current: #{RedmineInstaller::VERSION}). Please run `gem install redmine-installer`." 60 | end 61 | end 62 | 63 | private 64 | 65 | def _options 66 | config_file = File.join(@redmine.root, CONFIG_DIR, 'redmine-installer.yaml') 67 | 68 | if File.exist?(config_file) 69 | YAML.load_file(config_file, aliases: true) 70 | else 71 | {} 72 | end 73 | end 74 | 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/redmine-installer/patches/ruby.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | 3 | def blank? 4 | false 5 | end 6 | 7 | def present? 8 | true 9 | end 10 | 11 | end 12 | 13 | class String 14 | 15 | def blank? 16 | empty? 17 | end 18 | 19 | def present? 20 | !blank? 21 | end 22 | 23 | end 24 | 25 | class NilClass 26 | 27 | def blank? 28 | true 29 | end 30 | 31 | def present? 32 | false 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /lib/redmine-installer/patches/tty.rb: -------------------------------------------------------------------------------- 1 | # module TTY 2 | # class ProgressBar 3 | # # Every progressbar register new signal handlers 4 | # def register_signals 5 | # end 6 | # end 7 | # end 8 | 9 | # module TTY 10 | # class Prompt 11 | # class List 12 | # def keyescape(*) 13 | # end 14 | # end 15 | # end 16 | # end 17 | -------------------------------------------------------------------------------- /lib/redmine-installer/profile.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'yaml' 3 | 4 | module RedmineInstaller 5 | class Profile < OpenStruct 6 | PROFILES_FILE = File.join(Dir.home, '.redmine-installer-profiles.yml') 7 | 8 | def self.get!(profile_id) 9 | data = YAML.load_file(PROFILES_FILE, aliases: true) rescue nil 10 | 11 | if data.is_a?(Hash) && data.has_key?(profile_id) 12 | Profile.new(profile_id, data[profile_id]) 13 | else 14 | raise RedmineInstaller::ProfileError, "Profile ID=#{profile_id} does not exist" 15 | end 16 | end 17 | 18 | attr_reader :id 19 | 20 | def initialize(id=nil, data={}) 21 | super(data) 22 | @id = id 23 | end 24 | 25 | def save 26 | FileUtils.touch(PROFILES_FILE) 27 | 28 | all_data = YAML.load_file(PROFILES_FILE, aliases: true) 29 | all_data = {} unless all_data.is_a?(Hash) 30 | 31 | @id ||= all_data.keys.last.to_i + 1 32 | 33 | all_data[@id] = to_h 34 | 35 | File.write(PROFILES_FILE, YAML.dump(all_data)) 36 | 37 | puts "Profile was saved under ID=#{@id}" 38 | rescue => e 39 | puts "Profile could not be save due to #{e.message}" 40 | end 41 | 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/redmine-installer/redmine.rb: -------------------------------------------------------------------------------- 1 | require 'find' 2 | require 'io/console' 3 | 4 | module RedmineInstaller 5 | class Redmine < TaskModule 6 | 7 | attr_reader :database 8 | attr_accessor :root 9 | 10 | REQUIRED_FILES = [ 11 | 'app', 12 | 'lib', 13 | 'config', 14 | 'public', 15 | 'db', 16 | 'Gemfile', 17 | 'Rakefile', 18 | 'config.ru', 19 | File.join('lib', 'redmine'), 20 | File.join('lib', 'redmine.rb'), 21 | ] 22 | 23 | DEFAULT_BACKUP_ROOT = File.join(Dir.home, 'redmine-backups') 24 | BACKUP_EXCLUDE_FILES = ['log/', 'tmp/'] 25 | 26 | CHECK_N_INACCESSIBLE_FILES = 10 27 | 28 | FILES_DIR = 'files' 29 | 30 | def initialize(task, root=nil) 31 | super(task) 32 | @root = root.to_s 33 | 34 | if (dump = task.options.database_dump) 35 | @database_dump_to_load = File.expand_path(dump) 36 | end 37 | end 38 | 39 | def database_yml_path 40 | File.join(root, 'config', 'database.yml') 41 | end 42 | 43 | def configuration_yml_path 44 | File.join(root, 'config', 'configuration.yml') 45 | end 46 | 47 | def gemfile_local_path 48 | File.join(root, 'Gemfile.local') 49 | end 50 | 51 | def files_path 52 | File.join(root, FILES_DIR) 53 | end 54 | 55 | def plugins_path 56 | File.join(root, 'plugins') 57 | end 58 | 59 | def easy_plugins_path 60 | File.join(plugins_path, 'easyproject', 'easy_plugins') 61 | end 62 | 63 | def log_path 64 | File.join(root, 'log') 65 | end 66 | 67 | def bundle_path 68 | File.join(root, '.bundle') 69 | end 70 | 71 | def pids_files 72 | Dir.glob(File.join(root, 'tmp', 'pids', '*')) 73 | end 74 | 75 | def running? 76 | pids_files.any? 77 | end 78 | 79 | def load_profile(profile) 80 | @root = profile.redmine_root if root.empty? 81 | @backup_type = profile.backup_type 82 | @backup_root = profile.backup_root || profile.backup_dir 83 | 84 | # Convert setting from v1 85 | case @backup_type 86 | when :full_backup 87 | @backup_type = :full 88 | when :backup, :only_database 89 | @backup_type = :database 90 | end 91 | 92 | # Only valid setting 93 | unless [:full, :database, :nothing].include?(@backup_type) 94 | @backup_type = nil 95 | end 96 | end 97 | 98 | def save_profile(profile) 99 | profile.redmine_root = @root 100 | profile.backup_type = @backup_type 101 | profile.backup_root = @backup_root 102 | end 103 | 104 | # Ask for REDMINE_ROOT (if wasnt set) and check access rights 105 | # 106 | def ensure_and_valid_root 107 | if root.empty? 108 | puts 109 | @root = prompt.ask('Path to redmine root:', required: true, default: 'redmine') 110 | end 111 | 112 | @root = File.expand_path(@root) 113 | 114 | unless Dir.exist?(@root) 115 | create_dir(@root) 116 | end 117 | 118 | logger.info("REDMINE_ROOT: #{@root}") 119 | 120 | unreadable_files = [] 121 | all_directories = [] 122 | 123 | Find.find(@root).each do |item| 124 | if unreadable_files.size > CHECK_N_INACCESSIBLE_FILES 125 | break 126 | end 127 | 128 | # Installer only need read permission for a few files 129 | # but for sure it checks all of them 130 | if !File.readable?(item) 131 | unreadable_files << item 132 | next 133 | end 134 | 135 | # Actualy this permission should not be needed 136 | # becase deletable is checked by parent directory 137 | # if !File.writable?(item) 138 | # unreadable_files << item 139 | # end 140 | 141 | # Parent directory of the root can have any permission 142 | if item != @root 143 | all_directories << File.dirname(item) 144 | end 145 | end 146 | 147 | if unreadable_files.any? 148 | error "Application root contains unreadable files. Make sure that all files in #{@root} are readable for user #{env_user} (limit #{CHECK_N_INACCESSIBLE_FILES} files: #{unreadable_files.join(', ')})" 149 | end 150 | 151 | unwritable_directories = [] 152 | 153 | all_directories.uniq! 154 | all_directories.each do |item| 155 | if !File.writable?(item) 156 | unwritable_directories << item 157 | end 158 | end 159 | 160 | if unwritable_directories.any? 161 | error "Application root contains unwritable directories. Make sure that all directories in #{@root} are writable for user #{env_user} (limit #{CHECK_N_INACCESSIBLE_FILES} files: #{unwritable_directories.join(', ')})" 162 | end 163 | end 164 | 165 | # Check if redmine is running based on PID files. 166 | # 167 | def check_running_state 168 | if running? 169 | if prompt.yes?("Your app is running based on PID files (#{pids_files.join(', ')}). Do you want continue?", default: false) 170 | logger.warn("App is running (pids: #{pids_files.join(', ')}). Ignore it and continue.") 171 | else 172 | error('App is running') 173 | end 174 | end 175 | end 176 | 177 | # Create and configure rails database 178 | # 179 | def create_database_yml 180 | print_title('Creating database configuration') 181 | 182 | @database = Database.create_config(self) 183 | logger.info("Database initialized #{@database}") 184 | end 185 | 186 | # Create and configure configuration 187 | # For now only email 188 | # 189 | def create_configuration_yml 190 | print_title('Creating email configuration') 191 | 192 | @configuration = Configuration.create_config(self) 193 | logger.info("Configuration initialized #{@configuration}") 194 | end 195 | 196 | # Run install commands (command might ask for additional informations) 197 | # 198 | def install 199 | print_title('Redmine installing') 200 | 201 | Dir.chdir(root) do 202 | # Gems can be locked on bad version 203 | FileUtils.rm_f('Gemfile.lock') 204 | 205 | # Install new gems 206 | bundle_install 207 | 208 | # Generate secret token 209 | rake_generate_secret_token 210 | 211 | # Ensuring database 212 | rake_db_create 213 | 214 | # Load database dump (if was set via CLI or attach on package) 215 | load_database_dump 216 | 217 | # Migrating 218 | rake_db_migrate 219 | 220 | # Plugin migrating 221 | rake_redmine_plugin_migrate 222 | 223 | # Install easyproject 224 | rake_easyproject_install if easyproject? 225 | end 226 | end 227 | 228 | def upgrade 229 | print_title('Redmine upgrading') 230 | 231 | Dir.chdir(root) do 232 | # Gems can be locked on bad version 233 | FileUtils.rm_f('Gemfile.lock') 234 | 235 | # Install new gems 236 | bundle_install 237 | 238 | # Generate secret token 239 | rake_generate_secret_token 240 | 241 | # Migrating 242 | rake_db_migrate 243 | 244 | # Plugin migrating 245 | rake_redmine_plugin_migrate 246 | 247 | # Install easyproject 248 | rake_easyproject_install if easyproject? 249 | end 250 | end 251 | 252 | def restore_db 253 | print_title('Database restoring') 254 | 255 | @database = Database.init(self) 256 | 257 | Dir.chdir(root) do 258 | # Load database dump (if was set via CLI) 259 | load_database_dump 260 | 261 | # Migrating 262 | rake_db_migrate 263 | 264 | # Plugin migrating 265 | rake_redmine_plugin_migrate 266 | 267 | # Install easyproject 268 | rake_easyproject_install if easyproject? 269 | end 270 | end 271 | 272 | # # => ['.', '..'] 273 | # def empty_root? 274 | # Dir.entries(root).size <= 2 275 | # end 276 | 277 | def delete_root 278 | Dir.chdir(root) do 279 | Dir.entries('.').each do |entry| 280 | next if entry == '.' || entry == '..' 281 | next if entry == FILES_DIR && task.options.copy_files_with_symlink 282 | 283 | begin 284 | FileUtils.remove_entry_secure(entry) 285 | rescue 286 | if task.options.copy_files_with_symlink 287 | print_title('Cannot automatically empty ' + root + '. Please manually empty this directory (except for ' + root + '/' + FILES_DIR + ') and then hit any key to continue...') 288 | else 289 | print_title('Cannot automatically empty ' + root + '. Please manually empty this directory and then hit any key to continue...') 290 | end 291 | STDIN.getch 292 | break 293 | end 294 | end 295 | end 296 | 297 | logger.info("#{root} content was deleted") 298 | end 299 | 300 | def move_from(other_redmine) 301 | Dir.chdir(other_redmine.root) do 302 | 303 | # Bundler save plugin with absolute paths 304 | # which is not pointing to the temporary directory 305 | bundle_index = File.join(Dir.pwd, '.bundle/plugin/index') 306 | 307 | if File.exist?(bundle_index) 308 | index = YAML.load_file(bundle_index, aliases: true) 309 | 310 | # { load_paths: { PLUGIN_NAME: *PATHS } } 311 | # 312 | if index.has_key?('load_paths') 313 | load_paths = index['load_paths'] 314 | if load_paths.is_a?(Hash) 315 | load_paths.each do |_, paths| 316 | paths.each do |path| 317 | path.sub!(other_redmine.root, root) 318 | end 319 | end 320 | end 321 | end 322 | 323 | # { plugin_paths: { PLUGIN_NAME: PATH } } 324 | # 325 | if index.has_key?('plugin_paths') 326 | plugin_paths = index['plugin_paths'] 327 | if plugin_paths.is_a?(Hash) 328 | plugin_paths.each do |_, path| 329 | path.sub!(other_redmine.root, root) 330 | end 331 | end 332 | end 333 | 334 | File.write(bundle_index, index.to_yaml) 335 | 336 | logger.info("Bundler plugin index from #{other_redmine.root} into #{root}") 337 | else 338 | logger.info("Bundler plugin index from #{other_redmine.root} not found") 339 | end 340 | 341 | Dir.entries('.').each do |entry| 342 | next if entry == '.' || entry == '..' 343 | 344 | if entry == FILES_DIR && task.options.copy_files_with_symlink 345 | FileUtils.rm(entry) 346 | else 347 | FileUtils.mv(entry, root) 348 | end 349 | end 350 | end 351 | 352 | logger.info("Copyied from #{other_redmine.root} into #{root}") 353 | end 354 | 355 | # Copy important files which cannot be deleted 356 | # 357 | def copy_importants_from(other_redmine) 358 | Dir.chdir(root) do 359 | # Copy database.yml 360 | FileUtils.cp(other_redmine.database_yml_path, database_yml_path) 361 | 362 | # Copy configuration.yml 363 | if File.exist?(other_redmine.configuration_yml_path) 364 | FileUtils.cp(other_redmine.configuration_yml_path, configuration_yml_path) 365 | end 366 | 367 | # Copy Gemfile.local 368 | if File.exist?(other_redmine.gemfile_local_path) 369 | FileUtils.cp(other_redmine.gemfile_local_path, gemfile_local_path) 370 | end 371 | 372 | # Copy files 373 | if task.options.copy_files_with_symlink 374 | FileUtils.rm_rf(files_path) 375 | FileUtils.ln_s(other_redmine.files_path, root) 376 | else 377 | FileUtils.cp_r(other_redmine.files_path, root) 378 | end 379 | 380 | # Copy old logs 381 | FileUtils.mkdir_p(log_path) 382 | Dir.glob(File.join(other_redmine.log_path, 'redmine_installer_*')).each do |log| 383 | FileUtils.cp(log, log_path) 384 | end 385 | 386 | # Copy bundle config 387 | if Dir.exist?(other_redmine.bundle_path) 388 | FileUtils.mkdir_p(bundle_path) 389 | FileUtils.cp_r(other_redmine.bundle_path, root) 390 | end 391 | end 392 | 393 | # Copy 'keep' files (base on options) 394 | Array(task.options.keep).each do |path| 395 | origin_path = File.join(other_redmine.root, path) 396 | next unless File.exist?(origin_path) 397 | 398 | # Ensure folder 399 | target_dir = File.join(root, File.dirname(path)) 400 | FileUtils.mkdir_p(target_dir) 401 | 402 | # Copy recursive 403 | FileUtils.cp_r(origin_path, target_dir) 404 | end 405 | 406 | logger.info('Important files was copyied') 407 | end 408 | 409 | def yield_missing_plugins(source_directory, target_directory) 410 | if !Dir.exist?(source_directory) 411 | return 412 | end 413 | 414 | Dir.chdir(source_directory) do 415 | Dir.entries('.').each do |plugin| 416 | next if plugin == '.' || plugin == '..' 417 | 418 | # Plugin is not directory 419 | unless File.directory?(plugin) 420 | next 421 | end 422 | 423 | to = File.join(target_directory, plugin) 424 | 425 | # Plugins does not exist 426 | unless Dir.exist?(to) 427 | yield File.expand_path(plugin), File.expand_path(to) 428 | end 429 | end 430 | end 431 | end 432 | 433 | # New package may not have all plugins 434 | # 435 | def copy_missing_plugins_from(other_redmine) 436 | missing = [] 437 | 438 | yield_missing_plugins(other_redmine.plugins_path, plugins_path) do |from, to| 439 | missing << [from, to] 440 | end 441 | 442 | yield_missing_plugins(other_redmine.easy_plugins_path, easy_plugins_path) do |from, to| 443 | missing << [from, to] 444 | end 445 | 446 | missing_plugin_names = missing.map{|(from, to)| File.basename(from) } 447 | logger.info("Missing plugins: #{missing_plugin_names.join(', ')}") 448 | 449 | if missing.empty? 450 | return 451 | end 452 | 453 | puts 454 | if !prompt.yes?("Your application contains plugins that are not present in the package (#{missing_plugin_names.join(', ')}). Would you like to copy them?") 455 | return 456 | end 457 | 458 | missing.each do |(from, to)| 459 | FileUtils.cp_r(from, to) 460 | logger.info("Copied #{from} to #{to}") 461 | end 462 | end 463 | 464 | def validate 465 | # Check for required files 466 | Dir.chdir(root) do 467 | REQUIRED_FILES.each do |path| 468 | unless File.exist?(path) 469 | error "Redmine #{root} is not valid. Directory '#{path}' is missing." 470 | end 471 | end 472 | end 473 | 474 | # Plugins are in right dir 475 | Dir.glob(File.join(root, 'vendor', 'plugins', '*')).each do |path| 476 | if File.directory?(path) 477 | error "Plugin should be on plugins dir. On vendor/plugins is #{path}" 478 | end 479 | end 480 | end 481 | 482 | # Backup: 483 | # - full redmine (except log, tmp) 484 | # - production database 485 | def make_backup 486 | print_title('Data backup') 487 | 488 | @backup_type ||= prompt.select('What type of backup do you want?', 489 | 'Full (redmine root and database)' => :full, 490 | 'Only database' => :database, 491 | 'Nothing' => :nothing) 492 | 493 | logger.info("Backup type: #{@backup_type}") 494 | 495 | # Dangerous option 496 | if @backup_type == :nothing 497 | if prompt.yes?('Are you sure you dont want backup?', default: false) 498 | logger.info('Backup option nothing was confirmed') 499 | return 500 | else 501 | @backup_type = nil 502 | return make_backup 503 | end 504 | end 505 | 506 | @backup_root ||= prompt.ask('Where to save backup:', required: true, default: DEFAULT_BACKUP_ROOT) 507 | @backup_root = File.expand_path(@backup_root) 508 | 509 | @backup_dir = File.join(@backup_root, Time.now.strftime('backup_%d%m%Y_%H%M%S')) 510 | create_dir(@backup_dir) 511 | 512 | files_to_backup = [] 513 | Dir.chdir(root) do 514 | case @backup_type 515 | when :full 516 | files_to_backup = Dir.glob(File.join('**', '{*,.*}')) 517 | end 518 | end 519 | 520 | if files_to_backup.any? 521 | files_to_backup.delete_if do |path| 522 | path.start_with?(*BACKUP_EXCLUDE_FILES) 523 | end 524 | 525 | @backup_package = File.join(@backup_dir, 'redmine.zip') 526 | 527 | Dir.chdir(root) do 528 | puts 529 | puts 'Files backuping' 530 | Zip::File.open(@backup_package, Zip::File::CREATE) do |zipfile| 531 | progressbar = TTY::ProgressBar.new(PROGRESSBAR_FORMAT, total: files_to_backup.size, frequency: 2, clear: true) 532 | 533 | files_to_backup.each do |entry| 534 | zipfile.add(entry, entry) 535 | progressbar.advance(1) 536 | end 537 | 538 | progressbar.finish 539 | end 540 | end 541 | 542 | puts "Files backed up on #{@backup_package}" 543 | logger.info('Files backed up') 544 | end 545 | 546 | @database = Database.init(self) 547 | @database.make_backup(@backup_dir) 548 | 549 | puts "Database backed up on #{@database.backup}" 550 | logger.info('Database backed up') 551 | end 552 | 553 | def valid_options 554 | if @database_dump_to_load && !(File.exist?(@database_dump_to_load) && File.file?(@database_dump_to_load)) 555 | error "Database dump #{@database_dump_to_load} does not exist (path is expanded)." 556 | end 557 | end 558 | 559 | def clean_up 560 | end 561 | 562 | private 563 | 564 | def bundle_install 565 | gemfile = File.join(root, 'Gemfile') 566 | status = run_command("bundle install #{task.options.bundle_options} --gemfile #{gemfile}", 'Bundle install') 567 | 568 | # Even if bundle could not install all gem EXIT_SUCCESS is returned 569 | if !status || !File.exist?('Gemfile.lock') 570 | puts 571 | selected = prompt.select("Gemfile.lock wasn't created. Please choose one option:", 572 | 'Try again' => :try_again, 573 | 'Change bundle options' => :change_options, 574 | 'Cancel' => :cancel) 575 | 576 | case selected 577 | when :try_again 578 | bundle_install 579 | when :change_options 580 | task.options.bundle_options = prompt.ask('New options:', default: task.options.bundle_options) 581 | bundle_install 582 | when :cancel 583 | error('Operation canceled by user') 584 | end 585 | end 586 | end 587 | 588 | def rake_db_create 589 | # Always return 0 590 | run_command('RAILS_ENV=production bundle exec rake db:create', 'Database creating') 591 | end 592 | 593 | def rake_db_migrate 594 | status = run_command('RAILS_ENV=production bundle exec rake db:migrate', 'Database migrating') 595 | 596 | unless status 597 | puts 598 | selected = prompt.select('Migration end with error. Please choose one option:', 599 | 'Try again' => :try_again, 600 | 'Create database first' => :create_database, 601 | 'Change database configuration' => :change_configuration, 602 | 'Cancel' => :cancel) 603 | 604 | case selected 605 | when :try_again 606 | rake_db_migrate 607 | when :create_database 608 | rake_db_create 609 | rake_db_migrate 610 | when :change_configuration 611 | create_database_yml 612 | rake_db_migrate 613 | when :cancel 614 | error('Operation canceled by user') 615 | end 616 | end 617 | end 618 | 619 | def rake_redmine_plugin_migrate 620 | status = run_command('RAILS_ENV=production bundle exec rake redmine:plugins:migrate', 'Plugins migration') 621 | 622 | unless status 623 | puts 624 | selected = prompt.select('Plugin migration end with error. Please choose one option:', 625 | 'Try again' => :try_again, 626 | 'Continue' => :continue, 627 | 'Cancel' => :cancel) 628 | 629 | case selected 630 | when :try_again 631 | rake_redmine_plugin_migrate 632 | when :continue 633 | logger.warn('Plugin migration end with error but step was skipped.') 634 | when :cancel 635 | error('Operation canceled by user') 636 | end 637 | end 638 | end 639 | 640 | def rake_generate_secret_token 641 | status = run_command('RAILS_ENV=production bundle exec rake generate_secret_token', 'Generating secret token') 642 | 643 | unless status 644 | puts 645 | selected = prompt.select('Secret token could not be created. Please choose one option:', 646 | 'Try again' => :try_again, 647 | 'Continue' => :continue, 648 | 'Cancel' => :cancel) 649 | 650 | case selected 651 | when :try_again 652 | rake_generate_secret_token 653 | when :continue 654 | logger.warn('Secret token could not be created but step was skipped.') 655 | when :cancel 656 | error('Operation canceled by user') 657 | end 658 | end 659 | end 660 | 661 | def rake_easyproject_install 662 | status = without_env('NAME') { 663 | run_command('RAILS_ENV=production bundle exec rake easyproject:install', 'Installing easyproject') 664 | } 665 | 666 | unless status 667 | puts 668 | selected = prompt.select('Easyproject could not be installed. Please choose one option:', 669 | 'Try again' => :try_again, 670 | 'Cancel' => :cancel) 671 | 672 | case selected 673 | when :try_again 674 | rake_easyproject_install 675 | when :cancel 676 | error('Operation canceled by user') 677 | end 678 | end 679 | end 680 | 681 | def easyproject? 682 | Dir.entries(plugins_path).include?('easyproject') 683 | end 684 | 685 | def load_database_dump_from_file 686 | selected = prompt.select('Database dump will be loaded. Before that all data must be destroy.', 687 | 'Skip dump loading' => :cancel, 688 | 'I am aware of this. Want to continue' => :continue) 689 | 690 | if selected == :continue 691 | @database.do_restore(@database_dump_to_load) 692 | logger.info('Database dump was loaded.') 693 | else 694 | logger.info('Database dump loading was skipped.') 695 | end 696 | end 697 | 698 | def load_database_dump_from_package 699 | if !prompt.no?('Would you like to load default data? Warning: By choosing "yes", you are confirming that all your existing redmine data will be removed.') 700 | 701 | @database.do_restore(task.package_config.sql_dump_file) 702 | 703 | logger.info('Default database dump was loaded.') 704 | end 705 | end 706 | 707 | def load_database_dump 708 | if @database_dump_to_load 709 | load_database_dump_from_file 710 | elsif task.package_config.dump_attached? && task.package_config.dump_compatible?(@database) 711 | load_database_dump_from_package 712 | end 713 | end 714 | 715 | def without_env(*names) 716 | backup = ENV.clone.to_hash 717 | ENV.delete_if {|key, _| names.include?(key) } 718 | yield 719 | ensure 720 | ENV.replace(backup) 721 | end 722 | 723 | end 724 | end 725 | -------------------------------------------------------------------------------- /lib/redmine-installer/restore_db.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | class RestoreDB < Task 3 | 4 | def initialize(database_dump, redmine_root) 5 | super(database_dump: database_dump.to_s) 6 | 7 | @environment = Environment.new(self) 8 | @redmine = Redmine.new(self, redmine_root) 9 | end 10 | 11 | def up 12 | @environment.check 13 | 14 | @redmine.valid_options 15 | @redmine.ensure_and_valid_root 16 | @redmine.validate 17 | @redmine.check_running_state 18 | @redmine.restore_db 19 | 20 | puts 21 | puts pastel.bold('Database was restored') 22 | logger.info('Database was restored') 23 | end 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/redmine-installer/spec/spec.rb: -------------------------------------------------------------------------------- 1 | $stdin.sync = true 2 | $stdout.sync = true 3 | 4 | module TTY::Cursor 5 | 6 | singleton_methods.each do |name| 7 | class_eval <<-METHODS 8 | 9 | def #{name}(*) 10 | '' 11 | end 12 | 13 | def self.#{name}(*) 14 | '' 15 | end 16 | 17 | METHODS 18 | end 19 | 20 | end 21 | 22 | class TestPrompt < TTY::Prompt 23 | 24 | def initialize(*args) 25 | super 26 | @enabled_color = false 27 | @pastel = Pastel.new(enabled: false) 28 | end 29 | 30 | end 31 | 32 | class TTY::Reader::Console 33 | 34 | def get_char(options) 35 | input.getc 36 | end 37 | 38 | end 39 | 40 | RedmineInstaller.instance_variable_set(:@prompt, TestPrompt.new) 41 | RedmineInstaller.instance_variable_set(:@pastel, Pastel.new(enabled: false)) 42 | -------------------------------------------------------------------------------- /lib/redmine-installer/task.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | class Task 3 | include Utils 4 | 5 | attr_reader :options 6 | 7 | def initialize(**options) 8 | @options = OpenStruct.new(options) 9 | 10 | logger.info "#{class_name} initialized with #{options}" 11 | logger.info "RUBY_VERSION: #{RUBY_VERSION}" 12 | logger.info "VERSION: #{RedmineInstaller::VERSION}" 13 | logger.info "USER: #{env_user}" 14 | end 15 | 16 | def run 17 | up 18 | rescue => e 19 | @error = e 20 | 21 | logger.error(e.message) 22 | logger.error(*e.backtrace) 23 | 24 | puts pastel.red(e.message) 25 | 26 | down 27 | end 28 | 29 | def up 30 | raise NotImplementedError 31 | end 32 | 33 | def down 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/redmine-installer/task_module.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | class TaskModule 3 | include Utils 4 | 5 | attr_reader :task 6 | 7 | def initialize(task) 8 | @task = task 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/redmine-installer/upgrade.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | class Upgrade < Install 3 | 4 | def up 5 | if options.profile 6 | @profile = Profile.get!(options.profile) 7 | end 8 | 9 | if @profile 10 | @target_redmine.load_profile(@profile) 11 | end 12 | 13 | @environment.check 14 | @target_redmine.ensure_and_valid_root 15 | @target_redmine.validate 16 | @target_redmine.check_running_state 17 | 18 | @package.ensure_and_valid_package 19 | @package.extract 20 | 21 | @temp_redmine.root = @package.redmine_root 22 | @package_config.check_version 23 | @temp_redmine.validate 24 | 25 | @target_redmine.make_backup 26 | 27 | @temp_redmine.copy_importants_from(@target_redmine) 28 | @temp_redmine.copy_missing_plugins_from(@target_redmine) 29 | 30 | @temp_redmine.upgrade 31 | 32 | print_title('Finishing installation') 33 | ok('Cleaning root'){ @target_redmine.delete_root } 34 | ok('Moving redmine to target directory'){ @target_redmine.move_from(@temp_redmine) } 35 | ok('Cleanning up'){ @package.clean_up } 36 | ok('Moving installer log'){ logger.move_to(@target_redmine, suffix: 'upgrade') } 37 | 38 | puts 39 | puts pastel.bold('Redmine was upgraded') 40 | logger.info('Redmine was upgraded') 41 | 42 | if @profile.nil? && prompt.yes?('Do you want save steps for further use?', default: false) 43 | profile = Profile.new 44 | @target_redmine.save_profile(profile) 45 | profile.save 46 | end 47 | end 48 | 49 | def down 50 | @temp_redmine.clean_up 51 | @package.clean_up 52 | 53 | if @target_redmine.database && @target_redmine.database.backuped? 54 | puts 55 | puts "Database have been backed up on #{pastel.bold(@target_redmine.database.backup)}" 56 | end 57 | 58 | puts 59 | puts "(Log is located on #{pastel.bold(logger.path)})" 60 | end 61 | 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/redmine-installer/utils.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | module Utils 3 | 4 | PROGRESSBAR_FORMAT = ':elapsed [:bar] :percent' 5 | 6 | def class_name 7 | self.class.name.split('::').last 8 | end 9 | 10 | def logger 11 | RedmineInstaller.logger 12 | end 13 | 14 | def prompt 15 | RedmineInstaller.prompt 16 | end 17 | 18 | def pastel 19 | RedmineInstaller.pastel 20 | end 21 | 22 | def run_command(cmd, title=nil) 23 | RedmineInstaller::Command.new(cmd, title: title).run 24 | end 25 | 26 | def ok(title) 27 | print "#{title} ... " 28 | yield 29 | puts pastel.green('OK') 30 | rescue => e 31 | puts pastel.red('FAIL') 32 | raise 33 | end 34 | 35 | def env_user 36 | ENV['USER'] || ENV['USERNAME'] 37 | end 38 | 39 | # Try create a dir 40 | # When mkdir raise an error (permission problem) method ask user if wants exist or try again 41 | def create_dir(dir) 42 | FileUtils.mkdir_p(dir) 43 | rescue 44 | if prompt.yes?("Dir #{dir} doesn't exist and can't be created. Try again?", default: true) 45 | create_dir(dir) 46 | else 47 | error('Dir creating was aborted by user.') 48 | end 49 | end 50 | 51 | def print_title(title) 52 | puts 53 | puts pastel.white.on_black(title) 54 | end 55 | 56 | def error(message) 57 | raise RedmineInstaller::Error, message 58 | end 59 | 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/redmine-installer/version.rb: -------------------------------------------------------------------------------- 1 | module RedmineInstaller 2 | 3 | VERSION = '3.0.4' 4 | 5 | end 6 | -------------------------------------------------------------------------------- /redmine-installer.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | 4 | require 'redmine-installer/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'redmine-installer' 8 | spec.version = RedmineInstaller::VERSION 9 | spec.authors = ['Ondřej Moravčík', 'Easy Software Ltd.'] 10 | spec.email = ['developers@easysoftware.com'] 11 | spec.summary = %q{Easy way how install/upgrade Redmine, EasyRedmine or EasyProject.} 12 | spec.description = %q{} 13 | spec.homepage = 'https://github.com/easyredmine/redmine-installer' 14 | spec.license = 'MIT' 15 | spec.metadata = { 16 | "source_code_uri" => "https://github.com/easyredmine/redmine-installer", 17 | "changelog_uri" => "https://github.com/easyredmine/redmine-installer/blob/master/CHANGELOG.md", 18 | } 19 | 20 | files = `git ls-files -z`.split("\x0") - Dir.glob("{spec/**/*,docker-compose.yml,.github/**/*}") 21 | spec.files = files 22 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 23 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 24 | spec.require_paths = ['lib'] 25 | 26 | spec.required_ruby_version = '>= 3.1.2' 27 | 28 | spec.add_runtime_dependency 'bundler', '>= 2.3.7' 29 | spec.add_runtime_dependency 'commander' 30 | spec.add_runtime_dependency 'pastel', '~> 0.8.0' 31 | spec.add_runtime_dependency 'rubyzip' 32 | spec.add_runtime_dependency 'tty-progressbar', '~> 0.18.2' 33 | spec.add_runtime_dependency 'tty-prompt', '~> 0.23.1' 34 | spec.add_runtime_dependency 'tty-spinner', '~> 0.8.0' 35 | 36 | spec.add_development_dependency 'childprocess', '~> 4.1.0' 37 | spec.add_development_dependency 'rake' 38 | spec.add_development_dependency 'rspec', '~> 3.11.0' 39 | end 40 | -------------------------------------------------------------------------------- /spec/custom_matchers.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/expectations' 2 | 3 | RSpec::Matchers.define :have_output do |text| 4 | match do |process| 5 | expect(process.get(text)).to include(text) 6 | end 7 | 8 | failure_message do |process| 9 | "expected that \"#{process.last_get_return}\" would contains: #{text}" 10 | end 11 | end 12 | 13 | RSpec::Matchers.define :have_output_in do |text, second| 14 | match do |process| 15 | expect(process.get(text, max_wait: second)).to include(text) 16 | end 17 | 18 | failure_message do |process| 19 | "expected that \"#{process.last_get_return}\" would contains: #{text} in #{second}s" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/installer_helper.rb: -------------------------------------------------------------------------------- 1 | module InstallerHelper 2 | 3 | def db_username 4 | ENV['MYSQL_USER'].to_s 5 | end 6 | 7 | def db_password 8 | ENV['MYSQL_PASSWORD'].to_s 9 | end 10 | 11 | def expected_output(text) 12 | expect(@process).to have_output(text) 13 | end 14 | 15 | def expected_output_in(text, max_wait) 16 | expect(@process).to have_output_in(text, max_wait) 17 | end 18 | 19 | def write(text) 20 | @process.write(text + "\n") 21 | end 22 | 23 | # Be carefull - this could have later unpredictable consequences on stdin 24 | def select_choice 25 | # @process.write(' ') 26 | @process.write("\r") 27 | # @process.write("\r\n") 28 | end 29 | 30 | def go_up 31 | @process.write("\e[A") 32 | end 33 | 34 | def go_down 35 | # write(TTY::Reader::Keys.keys[:down]) 36 | # write("\e[B") 37 | @process.write("\e[B") 38 | end 39 | 40 | def email_username 41 | 'username' 42 | end 43 | 44 | def email_password 45 | 'password' 46 | end 47 | 48 | def email_address 49 | 'address' 50 | end 51 | 52 | def email_port 53 | '123' 54 | end 55 | 56 | def email_domain 57 | 'domain' 58 | end 59 | 60 | def email_authentication 61 | 'plain' 62 | end 63 | 64 | def email_openssl_verify_mode 65 | 'none' 66 | end 67 | 68 | def email_enable_tls 69 | false 70 | end 71 | 72 | def email_enable_starttls 73 | true 74 | end 75 | 76 | def expected_successful_configuration(email: false) 77 | expected_output('Creating database configuration') 78 | expected_output('What database do you want use?') 79 | expected_output('‣ MySQL') 80 | 81 | # go_down 82 | # expected_output('‣ PostgreSQL') 83 | select_choice 84 | 85 | expected_output('Database:') 86 | write(ENV.fetch('MYSQL_DATABASE', 'test')) 87 | 88 | expected_output('Host: (localhost)') 89 | write(ENV['DB_HOST'].to_s) 90 | 91 | expected_output('Username:') 92 | write(db_username) 93 | 94 | expected_output('Password:') 95 | write(db_password) 96 | 97 | expected_output('Encoding: (utf8mb4)') 98 | write('') 99 | 100 | expected_output('Port: (3306)') 101 | write('') 102 | 103 | expected_output('Creating email configuration') 104 | expected_output('Which service to use for email sending?') 105 | 106 | if email 107 | go_up 108 | go_up 109 | go_up 110 | expected_output('‣ Custom configuration (SMTP)') 111 | select_choice 112 | 113 | expected_output('Username:') 114 | write(email_username) 115 | 116 | expected_output('Password:') 117 | write(email_password) 118 | 119 | expected_output('Address:') 120 | write(email_address) 121 | 122 | expected_output('Port:') 123 | write(email_port) 124 | 125 | expected_output('Domain:') 126 | write(email_domain) 127 | 128 | expected_output('Authentication:') 129 | write(email_authentication) 130 | 131 | expected_output('Openssl verify mode:') 132 | write(email_openssl_verify_mode) 133 | 134 | expected_output('Enable tls?: (y/N)') 135 | write(email_enable_tls ? 'y' : 'n') 136 | 137 | expected_output('Enable starttls?: (Y/n)') 138 | write(email_enable_starttls ? 'y' : 'n') 139 | else 140 | expected_output('‣ Nothing') 141 | select_choice 142 | end 143 | end 144 | 145 | def expected_successful_installation_or_upgrade(db_creating: false, after_create: nil) 146 | expected_output_in('--> Bundle install', 900) 147 | expected_output_in('--> Database creating', 900) if db_creating 148 | after_create&.call 149 | expected_output_in('--> Database migrating', 900) 150 | expected_output_in('--> Plugins migration', 900) 151 | expected_output_in('--> Generating secret token', 900) 152 | 153 | expected_output_in('Cleaning root ... OK', 900) 154 | expected_output_in('Moving redmine to target directory ... OK', 900) 155 | expected_output_in('Cleanning up ... OK', 900) 156 | expected_output_in('Moving installer log ... OK', 900) 157 | end 158 | 159 | def expected_successful_installation(**options) 160 | expected_output('Redmine installing') 161 | expected_successful_installation_or_upgrade(db_creating: true, **options) 162 | expected_output('Redmine was installed') 163 | end 164 | 165 | def expected_successful_upgrade 166 | expected_output('Redmine upgrading') 167 | expected_successful_installation_or_upgrade 168 | expected_output('Redmine was upgraded') 169 | 170 | expected_output('Do you want save steps for further use?') 171 | write('n') 172 | end 173 | 174 | def expected_redmine_version(version) 175 | Dir.chdir(@redmine_root) do 176 | out = `bundle exec rails r -e production 'puts Redmine::VERSION.to_s'` 177 | expect($?.success?).to be_truthy 178 | expect(out).to include(version) 179 | end 180 | end 181 | 182 | def expected_email_configuration 183 | Dir.chdir(@redmine_root) do 184 | configuration = YAML.load_file('config/configuration.yml', aliases: true)['default']['email_delivery'] 185 | smtp_settings = configuration['smtp_settings'] 186 | 187 | expect(configuration['delivery_method']).to eq(:smtp) 188 | expect(smtp_settings['address']).to eq(email_address) 189 | expect(smtp_settings['port']).to eq(email_port.to_i) 190 | expect(smtp_settings['authentication']).to eq(email_authentication.to_sym) 191 | expect(smtp_settings['domain']).to eq(email_domain) 192 | expect(smtp_settings['user_name']).to eq(email_username) 193 | expect(smtp_settings['password']).to eq(email_password) 194 | expect(smtp_settings['tls']).to eq(email_enable_tls) 195 | expect(smtp_settings['enable_starttls_auto']).to eq(email_enable_starttls) 196 | expect(smtp_settings['openssl_verify_mode']).to eq(email_openssl_verify_mode) 197 | end 198 | end 199 | 200 | def wait_for_stdin_buffer 201 | sleep 0.5 202 | end 203 | 204 | end 205 | -------------------------------------------------------------------------------- /spec/installer_process.rb: -------------------------------------------------------------------------------- 1 | require 'childprocess' 2 | 3 | class InstallerProcess 4 | 5 | attr_reader :stdout, :last_get_return 6 | 7 | def initialize(command, *args) 8 | tempfile_out = File.open('redmine-installer-out', 'w') 9 | tempfile_err = File.open('redmine-installer-err', 'w') 10 | 11 | tempfile_out.sync = true 12 | tempfile_err.sync = true 13 | 14 | args = args.flatten.compact 15 | 16 | if ['install', 'upgrade'].include?(command) 17 | args << '--bundle-options' << '--without rmagick' 18 | end 19 | 20 | redmine_bin_file = File.expand_path('../bin/redmine', __dir__) 21 | 22 | @process = ChildProcess.build(redmine_bin_file, command, *args) 23 | # @process = ChildProcess.build('bin/redmine', command, *args) 24 | # @process = ChildProcess.build('redmine', command, *args) 25 | @process.io.stdout = tempfile_out 26 | @process.io.stderr = tempfile_err 27 | @process.environment['REDMINE_INSTALLER_SPEC'] = '1' 28 | @process.environment['REDMINE_INSTALLER_LOGFILE'] = File.expand_path(File.join(File.dirname(__FILE__), '..', 'log.log')) 29 | @process.duplex = true 30 | @process.detach = true 31 | 32 | # Because of file description is shared with redmine installer 33 | # so changing posiiotn has effect fot both processes 34 | @stdout = File.open(tempfile_out.path) 35 | 36 | @last_get_return = '' 37 | @buffer = '' 38 | @seek = 0 39 | end 40 | 41 | def run 42 | Bundler.with_unbundled_env { 43 | start 44 | yield 45 | } 46 | ensure 47 | stop 48 | end 49 | 50 | def start 51 | @process.start 52 | end 53 | 54 | def stop 55 | @process.stop 56 | end 57 | 58 | def write(text) 59 | @process.io.stdin << text 60 | 61 | # @process.io.stdin.puts(text) 62 | # @process.io.stdin.flush 63 | 64 | # @process.io.stdin.syswrite(text + "\n") 65 | # @process.io.stdin.flush 66 | 67 | # @process.io.stdin.write_nonblock(text + "\n") 68 | end 69 | 70 | def get(text, **args) 71 | @last_get_return = _get(text, **args) 72 | end 73 | 74 | private 75 | 76 | # max_wait in s 77 | def _get(text, max_wait: 180) 78 | wait_to = Time.now + max_wait 79 | while Time.now < wait_to 80 | @buffer << @stdout.read 81 | index = @buffer.rindex(text) 82 | 83 | if index 84 | break 85 | # return @buffer.slice!(0, index+text.size) 86 | else 87 | sleep 0.5 88 | end 89 | end 90 | 91 | @buffer 92 | end 93 | 94 | end 95 | -------------------------------------------------------------------------------- /spec/lib/backup_restore_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | DATABASE_DUMP = File.join(Dir.tmpdir, 'redmine_installer_database_backup.sql') 4 | 5 | RSpec.describe 'RedmineInstaller backup / restore', order: :defined do 6 | 7 | let(:project_count) { 5 } 8 | 9 | it 'create backup', :install_first, command: 'backup' do 10 | # First ensure `project_count` project 11 | Dir.chdir(@redmine_root) do 12 | out = `bundle exec rails runner " 13 | Project.delete_all 14 | 15 | #{project_count}.times do |i| 16 | p = Project.new 17 | p.name = 'Test ' + i.to_s 18 | p.identifier = 'test_' + i.to_s 19 | p.save(validate: false) 20 | end 21 | 22 | puts Project.count 23 | "` 24 | expect($?.success?).to be_truthy 25 | expect(out.to_i).to eq(project_count) 26 | end 27 | 28 | # Backup database 29 | expected_output('Path to redmine root:') 30 | write(@redmine_root) 31 | expected_output('Data backup') 32 | go_down 33 | expected_output('‣ Only database') 34 | select_choice 35 | expected_output('Where to save backup:') 36 | write(@backup_dir) 37 | expected_output('Database backuping') 38 | expected_output('Database backed up') 39 | 40 | # Ensure 0 project (database is shared with all tests) 41 | Dir.chdir(@redmine_root) do 42 | out = `bundle exec rails runner " 43 | Project.delete_all 44 | puts Project.count 45 | "` 46 | expect($?.success?).to be_truthy 47 | expect(out.to_i).to eq(0) 48 | end 49 | 50 | # Save backup (after test end - all backup will be deleted) 51 | dump = Dir.glob(File.join(@backup_dir, '*', '*')).last 52 | expect(dump).to end_with("#{ENV.fetch('MYSQL_DATABASE', 'test')}.sql") 53 | 54 | FileUtils.rm_f(DATABASE_DUMP) 55 | FileUtils.cp(dump, DATABASE_DUMP) 56 | end 57 | 58 | it 'restore', command: 'install', args: [package_v503, '--database-dump', DATABASE_DUMP] do 59 | expected_output('Path to redmine root:') 60 | write(@redmine_root) 61 | 62 | expected_successful_configuration 63 | 64 | expected_output('Database dump will be loaded.') 65 | expected_output('‣ Skip dump loading') 66 | 67 | go_down 68 | expected_output('‣ I am aware of this.') 69 | select_choice 70 | 71 | expected_output_in('Redmine was installed', 240) 72 | 73 | Dir.chdir(@redmine_root) do 74 | out = `bundle exec rails runner "puts Project.count"` 75 | expect(out.to_i).to eq(project_count) 76 | end 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /spec/lib/install_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe RedmineInstaller::Install, command: 'install' do 4 | 5 | it 'unreadable file', args: [], skip: "because redmine docker run under root" do 6 | FileUtils.chmod(0000, @redmine_root) 7 | 8 | expected_output('Path to redmine root:') 9 | write(@redmine_root) 10 | 11 | expected_output('Application root contains unreadable files') 12 | 13 | FileUtils.chmod(0600, @redmine_root) 14 | end 15 | 16 | it 'unwritable directory', args: [], skip: "because redmine docker run under root" do 17 | directory = File.join(@redmine_root, 'directory') 18 | subdirectory = File.join(directory, 'subdirectory') 19 | 20 | FileUtils.mkdir_p(subdirectory) 21 | FileUtils.chmod(0444, directory) 22 | 23 | expected_output('Path to redmine root:') 24 | write(@redmine_root) 25 | 26 | expected_output('Application root contains unreadable files') 27 | 28 | FileUtils.chmod(0700, directory) 29 | end 30 | 31 | it 'non-exising package', args: [] do 32 | this_file = File.expand_path(File.join(File.dirname(__FILE__))) 33 | 34 | expected_output('Path to redmine root:') 35 | write(@redmine_root) 36 | 37 | expected_output('Path to package:') 38 | write(this_file) 39 | 40 | expected_output("File #{this_file} must have format: .zip, .gz, .tgz") 41 | end 42 | 43 | it 'non-existing zip package', args: [] do 44 | expected_output('Path to redmine root:') 45 | write(@redmine_root) 46 | 47 | expected_output('Path to package:') 48 | write('aaa.zip') 49 | 50 | expected_output("File doesn't exist") 51 | end 52 | 53 | it 'install without arguments', args: [] do 54 | expected_output('Path to redmine root:') 55 | write(@redmine_root) 56 | 57 | expected_output('Path to package:') 58 | write(package_v503) 59 | 60 | expected_output('Extracting redmine package') 61 | 62 | expected_successful_configuration(email: true) 63 | expected_successful_installation 64 | 65 | expected_redmine_version('5.0.3') 66 | expected_email_configuration 67 | end 68 | 69 | it 'download redmine', args: ['v5.0.3'] do 70 | expected_output('Path to redmine root:') 71 | write(@redmine_root) 72 | 73 | expected_output_in('Downloading https://www.redmine.org/releases/redmine-5.0.3.zip', 30) 74 | expected_output('Extracting redmine package') 75 | 76 | expected_successful_configuration 77 | expected_successful_installation 78 | 79 | expected_redmine_version('5.0.3') 80 | end 81 | 82 | it 'installing something else', args: [package_someting_else] do 83 | write(@redmine_root) 84 | 85 | expected_output('is not valid') 86 | end 87 | 88 | it 'bad database settings', args: [package_v503] do 89 | write(@redmine_root) 90 | 91 | expected_output('Creating database configuration') 92 | expected_output('‣ MySQL') 93 | select_choice 94 | 95 | write('test') 96 | write('') 97 | write('testtesttest') 98 | sleep 0.5 # wait for buffer 99 | write(db_password) 100 | write('') 101 | write('') 102 | 103 | expected_output('Creating email configuration') 104 | write('') 105 | 106 | expected_output('Redmine installing') 107 | expected_output_in('--> Database migrating', 360) 108 | expected_output('Migration end with error') 109 | expected_output('‣ Try again') 110 | 111 | go_down 112 | go_down 113 | expected_output('‣ Change database configuration') 114 | write('') 115 | 116 | expected_output('‣ MySQL') 117 | write('') 118 | 119 | write(ENV.fetch('MYSQL_DATABASE', 'test')) 120 | write(ENV['DB_HOST'].to_s) 121 | write(db_username) 122 | sleep 0.5 # wait for buffer 123 | write(db_password) 124 | write('') 125 | write('') 126 | 127 | expected_output('--> Database migrating') 128 | expected_output_in('Redmine was installed', 360) 129 | 130 | expected_redmine_version('5.0.3') 131 | end 132 | 133 | it 'high installer version', args: [package_high_installer_version] do 134 | write(@redmine_root) 135 | expected_output('You are using an old version of installer') 136 | end 137 | 138 | it 'download redmine', args: [package_default_db], skip: "Only redmine 5.x is supported" do 139 | expected_output('Path to redmine root:') 140 | write(@redmine_root) 141 | 142 | expected_successful_configuration 143 | expected_successful_installation( 144 | after_create: proc { 145 | expected_output('Would you like to load default data') 146 | 147 | write('y') 148 | expected_output('Database cleaning') 149 | expected_output('Database restoring') 150 | } 151 | ) 152 | 153 | expected_redmine_version('3.4.5') 154 | 155 | Dir.chdir(@redmine_root) do 156 | out = `bundle exec rails runner "puts Issue.count"`.strip 157 | expect($?.success?).to be_truthy 158 | expect(out).to eq('3') 159 | end 160 | end 161 | 162 | end 163 | -------------------------------------------------------------------------------- /spec/lib/upgrade_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe RedmineInstaller::Upgrade, :install_first, command: 'upgrade' do 4 | 5 | it 'bad redmine root', args: [] do 6 | FileUtils.remove_entry(File.join(@redmine_root, 'app')) 7 | write(@redmine_root) 8 | 9 | expected_output("Redmine #{@redmine_root} is not valid.") 10 | end 11 | 12 | it 'upgrading with full backup' do 13 | # This should not be a problem because file still could be deleted 14 | unwritable_file = File.join(@redmine_root, 'unwritable_file') 15 | FileUtils.touch(unwritable_file) 16 | FileUtils.chmod(0400, unwritable_file) 17 | 18 | test_test_dir = File.join(@redmine_root, 'test_test') 19 | test_test_file = File.join(test_test_dir, 'test.txt') 20 | FileUtils.mkdir_p(test_test_dir) 21 | FileUtils.touch(test_test_file) 22 | 23 | expect(File.exist?(test_test_file)).to be_truthy 24 | 25 | expected_output('Path to redmine root:') 26 | write(@redmine_root) 27 | 28 | expected_output('Path to package:') 29 | write(package_v503) 30 | 31 | expected_output('Extracting redmine package') 32 | expected_output('Data backup') 33 | 34 | expected_output('‣ Full (redmine root and database)') 35 | select_choice 36 | 37 | expected_output('Where to save backup:') 38 | write(@backup_dir) 39 | 40 | expected_output('Files backuping') 41 | expected_output('Files backed up') 42 | expected_output('Database backuping') 43 | expected_output('Database backed up') 44 | 45 | expected_successful_upgrade 46 | 47 | expected_redmine_version('5.0.3') 48 | 49 | expect(File.exist?(test_test_file)).to be_falsey 50 | 51 | last_backup = Dir.glob(File.join(@backup_dir, '*')).sort.last 52 | backuped = Dir.glob(File.join(last_backup, '*')) 53 | 54 | expect(backuped.map { |f| File.zero?(f) }).to all(be_falsey) 55 | end 56 | 57 | it 'upgrade with no backup and files keeping', args: ['--keep', 'test_test'] do 58 | test_test_dir = File.join(@redmine_root, 'test_test') 59 | test_test_file = File.join(test_test_dir, 'test.txt') 60 | FileUtils.mkdir_p(test_test_dir) 61 | FileUtils.touch(test_test_file) 62 | 63 | expect(File.exist?(test_test_file)).to be_truthy 64 | 65 | wait_for_stdin_buffer 66 | write(@redmine_root) 67 | 68 | wait_for_stdin_buffer 69 | write(package_v503) 70 | 71 | wait_for_stdin_buffer 72 | 73 | go_down 74 | go_down 75 | expected_output('‣ Nothing') 76 | select_choice 77 | 78 | expected_output('Are you sure you dont want backup?') 79 | write('y') 80 | 81 | expected_successful_upgrade 82 | 83 | expected_redmine_version('5.0.3') 84 | 85 | expect(File.exist?(test_test_file)).to be_truthy 86 | end 87 | 88 | it 'copy files with symlink ', args: ['--copy-files-with-symlink'] do 89 | files_dir = File.join(@redmine_root, 'files') 90 | files = (0..10).map { |i| File.join(files_dir, "file_#{i}.txt") } 91 | FileUtils.touch(files) 92 | 93 | wait_for_stdin_buffer 94 | write(@redmine_root) 95 | 96 | wait_for_stdin_buffer 97 | write(package_v503) 98 | 99 | wait_for_stdin_buffer 100 | 101 | go_down 102 | go_down 103 | expected_output('‣ Nothing') 104 | select_choice 105 | 106 | expected_output('Are you sure you dont want backup?') 107 | write('y') 108 | 109 | expected_successful_upgrade 110 | 111 | expected_redmine_version('5.0.3') 112 | 113 | # Not bullet-prof but at least check if files are still there 114 | expect(Dir.glob(File.join(files_dir, '*.txt')).sort).to eq(files.sort) 115 | end 116 | 117 | it 'upgrade rys and modify bundle/index' do 118 | skip 'old version not supporting Ruby >3' 119 | wait_for_stdin_buffer 120 | write(@redmine_root) 121 | 122 | wait_for_stdin_buffer 123 | write(package_v345_rys) 124 | 125 | wait_for_stdin_buffer 126 | 127 | go_down 128 | go_down 129 | expected_output('‣ Nothing') 130 | select_choice 131 | 132 | expected_output('Are you sure you dont want backup?') 133 | write('y') 134 | 135 | expected_successful_upgrade 136 | 137 | expected_redmine_version('3.4.5') 138 | 139 | index = YAML.load_file(File.join(@redmine_root, '.bundle/plugin/index'), aliases: true) 140 | 141 | load_paths = index['load_paths']['rys-bundler'] 142 | expect(load_paths.size).to eq(1) 143 | 144 | load_path = load_paths.first 145 | expect(load_path).to start_with(@redmine_root) 146 | 147 | plugin_paths = index['plugin_paths']['rys-bundler'] 148 | expect(plugin_paths).to start_with(@redmine_root) 149 | end 150 | 151 | it 'upgrading something else' do 152 | wait_for_stdin_buffer 153 | write(@redmine_root) 154 | 155 | wait_for_stdin_buffer 156 | write(package_someting_else) 157 | 158 | wait_for_stdin_buffer 159 | expected_output('is not valid') 160 | end 161 | 162 | context 'missing plugins' do 163 | 164 | def upgrade_it(answer, result) 165 | # Create some plugins 166 | plugin_name = 'new_plugin' 167 | plugin_dir = File.join(@redmine_root, 'plugins', plugin_name) 168 | FileUtils.mkdir_p(plugin_dir) 169 | FileUtils.touch(File.join(plugin_dir, 'init.rb')) 170 | 171 | wait_for_stdin_buffer 172 | write(@redmine_root) 173 | 174 | wait_for_stdin_buffer 175 | write(package_v503) 176 | 177 | go_down 178 | go_down 179 | expected_output('‣ Nothing') 180 | select_choice 181 | write('y') 182 | 183 | expected_output("Your application contains plugins that are not present in the package (#{plugin_name}). Would you like to copy them?") 184 | 185 | write(answer) 186 | expected_successful_upgrade 187 | expected_redmine_version('5.0.3') 188 | 189 | expect(Dir.exist?(plugin_dir)).to be(result) 190 | end 191 | 192 | it 'yes' do 193 | upgrade_it('y', true) 194 | end 195 | 196 | it 'no' do 197 | upgrade_it('n', false) 198 | end 199 | 200 | end 201 | 202 | end 203 | -------------------------------------------------------------------------------- /spec/packages/high-installer-version.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyredmine/redmine-installer/4a9b1c9fe9c90b0f4c90b3583436f4edff739b72/spec/packages/high-installer-version.zip -------------------------------------------------------------------------------- /spec/packages/redmine-3.3.0-bad-migration.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyredmine/redmine-installer/4a9b1c9fe9c90b0f4c90b3583436f4edff739b72/spec/packages/redmine-3.3.0-bad-migration.zip -------------------------------------------------------------------------------- /spec/packages/redmine-3.4.5-default-db.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyredmine/redmine-installer/4a9b1c9fe9c90b0f4c90b3583436f4edff739b72/spec/packages/redmine-3.4.5-default-db.zip -------------------------------------------------------------------------------- /spec/packages/redmine-3.4.5-rys.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyredmine/redmine-installer/4a9b1c9fe9c90b0f4c90b3583436f4edff739b72/spec/packages/redmine-3.4.5-rys.zip -------------------------------------------------------------------------------- /spec/packages/redmine-3.4.5.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyredmine/redmine-installer/4a9b1c9fe9c90b0f4c90b3583436f4edff739b72/spec/packages/redmine-3.4.5.zip -------------------------------------------------------------------------------- /spec/packages/redmine-5.0.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyredmine/redmine-installer/4a9b1c9fe9c90b0f4c90b3583436f4edff739b72/spec/packages/redmine-5.0.3.zip -------------------------------------------------------------------------------- /spec/packages/something-else.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easyredmine/redmine-installer/4a9b1c9fe9c90b0f4c90b3583436f4edff739b72/spec/packages/something-else.zip -------------------------------------------------------------------------------- /spec/packages_helper.rb: -------------------------------------------------------------------------------- 1 | module PackagesHelper 2 | 3 | def packages_dir 4 | File.expand_path(File.join(__dir__, 'packages')) 5 | end 6 | 7 | def package_v345 8 | File.join(packages_dir, 'redmine-3.4.5.zip') 9 | end 10 | 11 | def package_v345_rys 12 | File.join(packages_dir, 'redmine-3.4.5-rys.zip') 13 | end 14 | 15 | def package_v503 16 | File.join(packages_dir, 'redmine-5.0.3.zip') 17 | end 18 | 19 | def package_someting_else 20 | File.join(packages_dir, 'something-else.zip') 21 | end 22 | 23 | def package_high_installer_version 24 | File.join(packages_dir, 'high-installer-version.zip') 25 | end 26 | 27 | def package_default_db 28 | File.join(packages_dir, 'redmine-3.4.5-default-db.zip') 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /spec/shared_contexts.rb: -------------------------------------------------------------------------------- 1 | RSpec.shared_context 'run installer' do 2 | 3 | around(:each) do |example| 4 | @process = InstallerProcess.new(example.metadata[:command], *((example.metadata[:args] || []) << "--enable_user_root")) 5 | @process.run do 6 | Dir.mktmpdir('redmine_root') do |dir| 7 | @redmine_root = dir 8 | example.run 9 | end 10 | end 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../gems.rb', __dir__) 2 | 3 | require 'bundler' 4 | Bundler.require(:default, :development) 5 | 6 | lib = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 7 | $LOAD_PATH.unshift(lib) if !$LOAD_PATH.include?(lib) 8 | 9 | require 'redmine-installer' 10 | 11 | require 'custom_matchers' 12 | require 'shared_contexts' 13 | 14 | require 'installer_process' 15 | require 'installer_helper' 16 | require 'packages_helper' 17 | 18 | RSpec.configure do |config| 19 | config.default_formatter = 'doc' 20 | config.color = true 21 | config.tty = true 22 | 23 | config.disable_monkey_patching! 24 | 25 | config.include_context 'run installer', :command 26 | 27 | config.include PackagesHelper 28 | config.extend PackagesHelper 29 | config.include InstallerHelper 30 | 31 | config.before(:all, :install_first) do 32 | @redmine_root = @origin_redmine = Dir.mktmpdir('redmine_root') 33 | @backup_dir = Dir.mktmpdir('backup_dir') 34 | @process = InstallerProcess.new('install', package_v503, @origin_redmine, "--enable_user_root") 35 | @process.run do 36 | expected_successful_configuration 37 | expected_successful_installation 38 | 39 | expected_redmine_version('5.0.3') 40 | end 41 | end 42 | 43 | config.after(:all, :install_first) do 44 | FileUtils.remove_entry(@origin_redmine, true) 45 | FileUtils.remove_entry(@backup_dir, true) 46 | end 47 | 48 | config.before(:each, :install_first) do 49 | FileUtils.cp_r(File.join(@origin_redmine, '.'), @redmine_root) 50 | end 51 | end 52 | --------------------------------------------------------------------------------