├── .gitignore
├── Makefile
├── .github
├── issue_template.md
└── workflows
│ └── unit_tests.yml
├── Schema
├── Sqlite.php
├── Postgres.php
└── Mysql.php
├── Test
└── PluginTest.php
├── Plugin.php
├── LICENSE
├── README.md
├── Command
└── MigrateCommand.php
└── DatabaseObjectStorage.php
/.gitignore:
--------------------------------------------------------------------------------
1 | *.zip
2 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all:
2 | @ echo "Build archive for plugin ${plugin} version=${version}"
3 | @ git archive HEAD --prefix=${plugin}/ --format=zip -o ${plugin}-${version}.zip
4 |
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 | **Please, do not create duplicate issues**
2 |
3 |
4 | ### Actual behaviour
5 |
6 |
7 | ### Expected behaviour
8 |
9 |
10 | ### Steps to reproduce
11 |
12 |
13 | ### Configuration
14 |
15 | - Plugin version:
16 | - Kanboard version:
17 | - Database type and version:
18 | - PHP version:
19 | - OS:
20 | - Browser:
21 |
22 |
--------------------------------------------------------------------------------
/Schema/Sqlite.php:
--------------------------------------------------------------------------------
1 | exec('CREATE TABLE IF NOT EXISTS object_storage (
12 | "object_key" VARCHAR(255) PRIMARY KEY,
13 | "object_data" BYTEA NOT NULL
14 | )');
15 | }
16 |
--------------------------------------------------------------------------------
/Schema/Postgres.php:
--------------------------------------------------------------------------------
1 | exec('CREATE TABLE IF NOT EXISTS object_storage (
12 | "object_key" VARCHAR(255) PRIMARY KEY,
13 | "object_data" BYTEA NOT NULL
14 | )');
15 | }
16 |
--------------------------------------------------------------------------------
/Schema/Mysql.php:
--------------------------------------------------------------------------------
1 | exec('CREATE TABLE IF NOT EXISTS `object_storage` (
12 | `object_key` VARCHAR(255) NOT NULL,
13 | `object_data` LONGBLOB NOT NULL,
14 | PRIMARY KEY (`object_key`)
15 | ) ENGINE=InnoDB
16 | ');
17 | }
18 |
--------------------------------------------------------------------------------
/Test/PluginTest.php:
--------------------------------------------------------------------------------
1 | container);
14 | $this->assertSame(null, $plugin->initialize());
15 | $this->assertNotEmpty($plugin->getPluginName());
16 | $this->assertNotEmpty($plugin->getPluginDescription());
17 | $this->assertNotEmpty($plugin->getPluginAuthor());
18 | $this->assertNotEmpty($plugin->getPluginVersion());
19 | $this->assertNotEmpty($plugin->getPluginHomepage());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Plugin.php:
--------------------------------------------------------------------------------
1 | container['objectStorage'] = function() {
13 | return new DatabaseObjectStorage($this->db);
14 | };
15 |
16 | $this->cli->add(new MigrateCommand($this->container));
17 | }
18 |
19 | public function getPluginName()
20 | {
21 | return 'Database Storage';
22 | }
23 |
24 | public function getPluginDescription()
25 | {
26 | return 'Use the database to store attachments';
27 | }
28 |
29 | public function getPluginAuthor()
30 | {
31 | return 'Frédéric Guillot';
32 | }
33 |
34 | public function getPluginVersion()
35 | {
36 | return '1.0.2';
37 | }
38 |
39 | public function getPluginHomepage()
40 | {
41 | return 'https://github.com/kanboard/plugin-database-storage';
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Frédéric Guillot
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Database Object Storage
2 | =======================
3 |
4 | This plugin stores uploaded files into the database instead of using the local filesystem.
5 |
6 | Author
7 | ------
8 |
9 | - Frederic Guillot
10 | - License MIT
11 |
12 | Requirements
13 | ------------
14 |
15 | - PHP >= 5.6
16 | - Kanboard >= 1.2.1
17 | - Postgres is recommended
18 | - Mysql or Sqlite
19 |
20 | Why I should store files in the database?
21 | -----------------------------------------
22 |
23 | Storing files in the database doesn't fit all usages.
24 | Obviously, the database size will increase over the time if you store large files.
25 |
26 | The main benefit of doing this is to simplify backups.
27 | Everything is in a central location and nothing is stored on the frontend servers.
28 | PostgreSQL is preferred because streaming files is supported.
29 |
30 | Migrating old files to the database
31 | -----------------------------------
32 |
33 | ```bash
34 | ./cli db-storage:migrate
35 | ```
36 |
37 | Notes
38 | -----
39 |
40 | - Run the command `VACUUM` to free up disk space used by removed files
41 | - With Mysql, you may need to change the value of `max_allowed_packet`, the default is 1MB
42 |
--------------------------------------------------------------------------------
/Command/MigrateCommand.php:
--------------------------------------------------------------------------------
1 | setName('db-storage:migrate')
20 | ->setDescription('Migrate local files to the database');
21 | }
22 |
23 | protected function execute(InputInterface $input, OutputInterface $output)
24 | {
25 | if (! file_exists(FILES_DIR)) {
26 | $output->writeln('Directory not found: ' . FILES_DIR . '');
27 | } else {
28 | $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(FILES_DIR), RecursiveIteratorIterator::SELF_FIRST);
29 | $storage = new DatabaseObjectStorage($this->container['db']);;
30 |
31 | foreach($files as $file) {
32 | $fileName = $file->getFilename();
33 | if ($fileName[0] !== '.' && ! $file->isDir()) {
34 | $key = substr($file->getRealPath(), strlen(FILES_DIR) + 1);
35 |
36 | if (! $this->fileExists($storage, $key)) {
37 | $output->writeln('Migrating ' . $fileName . '');
38 | $blob = $this->readFile($file->getRealPath());
39 | $storage->put($key, $blob);
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
46 | protected function fileExists(DatabaseObjectStorage $storage, $key)
47 | {
48 | try {
49 | $storage->get($key);
50 | return true;
51 | } catch (ObjectStorageException $e) {}
52 |
53 | return false;
54 | }
55 |
56 | protected function readFile($filename)
57 | {
58 | $handle = fopen($filename, 'rb');
59 | $contents = fread($handle, filesize($filename));
60 | fclose($handle);
61 | return $contents;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/.github/workflows/unit_tests.yml:
--------------------------------------------------------------------------------
1 | name: Unit Tests
2 |
3 | on:
4 | pull_request:
5 | branches: [ master ]
6 |
7 | jobs:
8 | Sqlite:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout Kanboard repo
12 | uses: actions/checkout@v2
13 | with:
14 | repository: kanboard/kanboard
15 | - name: Checkout Plugin repo
16 | uses: actions/checkout@v2
17 | with:
18 | path: plugins/DatabaseStorage
19 | - name: Install dependencies
20 | run: composer install --prefer-dist --no-progress --no-suggest
21 | - name: Unit tests with Sqlite
22 | run: ./vendor/bin/phpunit -c tests/units.sqlite.xml plugins/DatabaseStorage/Test/
23 |
24 | Postgres:
25 | runs-on: ubuntu-latest
26 | services:
27 | postgres:
28 | image: postgres:9.4
29 | env:
30 | POSTGRES_USER: postgres
31 | POSTGRES_PASSWORD: postgres
32 | POSTGRES_DB: kanboard_unit_test
33 | ports:
34 | - 5432:5432
35 | options: >-
36 | --health-cmd pg_isready
37 | --health-interval 10s
38 | --health-timeout 5s
39 | --health-retries 5
40 | steps:
41 | - name: Checkout Kanboard repo
42 | uses: actions/checkout@v2
43 | with:
44 | repository: kanboard/kanboard
45 | - name: Checkout Plugin repo
46 | uses: actions/checkout@v2
47 | with:
48 | path: plugins/DatabaseStorage
49 | - name: Install dependencies
50 | run: composer install --prefer-dist --no-progress --no-suggest
51 | - name: Unit tests with Postgres
52 | run: ./vendor/bin/phpunit -c tests/units.postgres.xml plugins/DatabaseStorage/Test/
53 | env:
54 | DB_HOSTNAME: 127.0.0.1
55 | DB_PORT: ${{ job.services.postgres.ports[5432] }}
56 |
57 | MySQL:
58 | runs-on: ubuntu-latest
59 | services:
60 | mysql:
61 | image: mysql:5.7
62 | env:
63 | MYSQL_ROOT_PASSWORD: "kanboard"
64 | MYSQL_DATABASE: "kanboard_unit_test"
65 | MYSQL_USER: "kanboard"
66 | MYSQL_PASSWORD: "kanboard"
67 | ports:
68 | - 3306:3306
69 | options: >-
70 | --health-cmd="mysqladmin ping"
71 | --health-interval 10s
72 | --health-timeout 5s
73 | --health-retries 10
74 | steps:
75 | - name: Checkout Kanboard repo
76 | uses: actions/checkout@v2
77 | with:
78 | repository: kanboard/kanboard
79 | - name: Checkout Plugin repo
80 | uses: actions/checkout@v2
81 | with:
82 | path: plugins/DatabaseStorage
83 | - name: Install dependencies
84 | run: composer install --prefer-dist --no-progress --no-suggest
85 | - name: Unit tests with MariaDB
86 | run: ./vendor/bin/phpunit -c tests/units.mysql.xml plugins/DatabaseStorage/Test/
87 | env:
88 | DB_HOSTNAME: 127.0.0.1
89 | DB_USERNAME: kanboard
90 | DB_PASSWORD: kanboard
91 | DB_NAME: kanboard_unit_test
92 | DB_PORT: ${{ job.services.mysql.ports[3306] }}
93 |
--------------------------------------------------------------------------------
/DatabaseObjectStorage.php:
--------------------------------------------------------------------------------
1 | db = $db;
33 | }
34 |
35 | /**
36 | * Fetch object contents
37 | *
38 | * @access public
39 | * @throws ObjectStorageException
40 | * @param string $key
41 | * @return string
42 | */
43 | public function get($key)
44 | {
45 | $contents = $this->db
46 | ->largeObject(self::TABLE)->eq('object_key', $key)
47 | ->findOneColumnAsString('object_data');
48 |
49 | if ($contents === false) {
50 | throw new ObjectStorageException('Object not found: '.$key);
51 | }
52 |
53 | return $contents;
54 | }
55 |
56 | /**
57 | * Save object
58 | *
59 | * @access public
60 | * @throws ObjectStorageException
61 | * @param string $key
62 | * @param string $blob
63 | */
64 | public function put($key, &$blob)
65 | {
66 | $result = $this->db->largeObject(self::TABLE)
67 | ->insertFromString('object_data', $blob, array('object_key' => $key));
68 |
69 | if ($result === false) {
70 | throw new ObjectStorageException('Unable to save object: '.$key);
71 | }
72 | }
73 |
74 | /**
75 | * Output directly object content
76 | *
77 | * @access public
78 | * @param string $key
79 | */
80 | public function output($key)
81 | {
82 | $fd = $this->db->largeObject(self::TABLE)->eq('object_key', $key)->findOneColumnAsStream('object_data');
83 |
84 | if (is_string($fd)) {
85 | echo $fd;
86 | } else {
87 | fpassthru($fd);
88 | }
89 | }
90 |
91 | /**
92 | * Move local file to object storage
93 | *
94 | * @access public
95 | * @throws ObjectStorageException
96 | * @param string $src_filename
97 | * @param string $key
98 | * @return boolean
99 | */
100 | public function moveFile($src_filename, $key)
101 | {
102 | $result = $this->db->largeObject(self::TABLE)
103 | ->insertFromFile('object_data', $src_filename, array('object_key' => $key));
104 |
105 | if ($result === false) {
106 | throw new ObjectStorageException('Unable to move this file: '.$src_filename);
107 | }
108 |
109 | return true;
110 | }
111 |
112 | /**
113 | * Move uploaded file to object storage
114 | *
115 | * @access public
116 | * @param string $filename
117 | * @param string $key
118 | * @return boolean
119 | */
120 | public function moveUploadedFile($filename, $key)
121 | {
122 | return $this->moveFile($filename, $key);
123 | }
124 |
125 | /**
126 | * Remove object
127 | *
128 | * @access public
129 | * @param string $key
130 | * @return boolean
131 | */
132 | public function remove($key)
133 | {
134 | return $this->db->table(self::TABLE)->eq('object_key', $key)->remove();
135 | }
136 | }
137 |
--------------------------------------------------------------------------------