Browse Source

Merge pull request #280 from tracim/dev/233/files_on_disk

Tracim 7 years ago
parent
commit
56cf2f4df8

+ 34 - 0
doc/migration.md View File

1
+# Performing migrations #
2
+
3
+## Introduction ##
4
+
5
+This document is intended to developers.
6
+
7
+Migrations on `Tracim` lays on [`gearbox migrate`](http://turbogears.readthedocs.io/en/tg2.3.7/turbogears/migrations.html), which in turn lays on [`alembic`](http://alembic.zzzcomputing.com/en/latest/index.html) which is the migration tool dedicated to `SQLAlchemy`.
8
+
9
+In order to use the `gearbox migrate [...]` commands, change your current directory to be `tracim/` from the root of the project, also usually named `tracim/` :
10
+
11
+    (tg2env) user@host:~/tracim$ cd tracim/
12
+    (tg2env) user@host:~/tracim/tracim$
13
+
14
+## Migration howto - Overview ##
15
+
16
+### Upgrading schema ###
17
+
18
+    gearbox migrate upgrade
19
+
20
+### Downgrading schema ###
21
+
22
+    gearbox migrate downgrade
23
+
24
+## Migration howto - Advanced (for developers) ##
25
+
26
+### Retrieving schema current version ###
27
+
28
+    gearbox migrate db_version
29
+
30
+### Creating new schema migration ###
31
+
32
+This creates a new python migration file in `tracim/migration/versions/` ending by `migration_label.py`:
33
+
34
+    gearbox migrate create 'migration label'

+ 18 - 5
tracim/migration/versions/69fb10c3d6f0_files_on_disk.py View File

1
-"""files on disk
1
+"""files on disk.
2
 
2
 
3
 Revision ID: 69fb10c3d6f0
3
 Revision ID: 69fb10c3d6f0
4
 Revises: c1cea4bbae16
4
 Revises: c1cea4bbae16
15
 down_revision = 'c1cea4bbae16'
15
 down_revision = 'c1cea4bbae16'
16
 
16
 
17
 
17
 
18
+# INFO - A.P - 2017-07-20 - alembic batch migrations
19
+# http://alembic.zzzcomputing.com/en/latest/batch.html
20
+# This migration uses alembic batch mode, a workaround allowing to enforce
21
+# ALTER statement with SQLite while maintaining the traditional behavior of
22
+# the commented lines on other RDBMS.
23
+
24
+
18
 def upgrade():
25
 def upgrade():
19
-    op.add_column('content_revisions',
20
-                  sa.Column('depot_file',
21
-                            UploadedFileField))
26
+    """Adds the depot file in revision."""
27
+    # op.add_column('content_revisions',
28
+    #               sa.Column('depot_file',
29
+    #                         UploadedFileField))
30
+    with op.batch_alter_table('content_revisions') as batch_op:
31
+        batch_op.add_column(sa.Column('depot_file', UploadedFileField))
22
 
32
 
23
 
33
 
24
 def downgrade():
34
 def downgrade():
25
-    op.drop_column('content_revisions', 'depot_file')
35
+    """Drops the depot file in revision."""
36
+    # op.drop_column('content_revisions', 'depot_file')
37
+    with op.batch_alter_table('content_revisions') as batch_op:
38
+        batch_op.drop_column('depot_file')

+ 87 - 0
tracim/migration/versions/913efdf409e5_all_files_also_on_disk.py View File

1
+"""all files also on disk.
2
+
3
+Revision ID: 913efdf409e5
4
+Revises: 69fb10c3d6f0
5
+Create Date: 2017-07-12 15:44:20.568447
6
+
7
+"""
8
+
9
+import shutil
10
+
11
+from alembic import op
12
+from depot.fields.sqlalchemy import UploadedFileField
13
+from depot.fields.upload import UploadedFile
14
+from depot.io.utils import FileIntent
15
+from depot.manager import DepotManager
16
+import sqlalchemy as sa
17
+
18
+# revision identifiers, used by Alembic.
19
+revision = '913efdf409e5'
20
+down_revision = '69fb10c3d6f0'
21
+
22
+
23
+revision_helper = sa.Table(
24
+    'content_revisions',
25
+    sa.MetaData(),
26
+    sa.Column('revision_id', sa.Integer, primary_key=True),
27
+    sa.Column('label', sa.String(1024), nullable=False),
28
+    sa.Column('file_extension', sa.String(255), nullable=False),
29
+    sa.Column('file_mimetype', sa.String(255), nullable=False),
30
+    sa.Column('file_content', sa.LargeBinary),
31
+    sa.Column('depot_file', UploadedFileField, nullable=True),
32
+    sa.Column('type', sa.String(32), nullable=False),
33
+)
34
+
35
+
36
+def delete_files_on_disk(connection: sa.engine.Connection):
37
+    """Deletes files from disk and their references in database."""
38
+    delete_query = revision_helper.update() \
39
+        .where(revision_helper.c.type == 'file') \
40
+        .where(revision_helper.c.depot_file.isnot(None)) \
41
+        .values(depot_file=None)
42
+    connection.execute(delete_query)
43
+    shutil.rmtree('depot/', ignore_errors=True)
44
+
45
+
46
+def upgrade():
47
+    """
48
+    Sets all depot files for file typed revisions.
49
+
50
+    Until now, files are both in database and, for the newly created
51
+    ones, on disk. In order to simplify the migration, this procedure
52
+    will:
53
+    - delete the few files on disk,
54
+    - create all files on disk from database.
55
+    """
56
+    # Creates files depot used in this migration
57
+    DepotManager.configure(
58
+        'tracim', {'depot.storage_path': 'depot/'},
59
+    )
60
+    connection = op.get_bind()
61
+    delete_files_on_disk(connection=connection)
62
+    select_query = revision_helper.select() \
63
+        .where(revision_helper.c.type == 'file') \
64
+        .where(revision_helper.c.depot_file.is_(None))
65
+    files = connection.execute(select_query).fetchall()
66
+    for file in files:
67
+        file_filename = '{0}{1}'.format(
68
+            file.label,
69
+            file.file_extension,
70
+        )
71
+        depot_file_intent = FileIntent(
72
+            file.file_content,
73
+            file_filename,
74
+            file.file_mimetype,
75
+        )
76
+        depot_file_field = UploadedFile(depot_file_intent, 'tracim')
77
+        update_query = revision_helper.update() \
78
+            .where(revision_helper.c.revision_id == file.revision_id) \
79
+            .values(depot_file=depot_file_field) \
80
+            .return_defaults()
81
+        connection.execute(update_query)
82
+
83
+
84
+def downgrade():
85
+    """Resets depot file for file typed revisions."""
86
+    connection = op.get_bind()
87
+    delete_files_on_disk(connection=connection)

+ 26 - 0
tracim/migration/versions/f3852e1349c4_all_files_only_on_disk.py View File

1
+"""all files only on disk
2
+
3
+Revision ID: f3852e1349c4
4
+Revises: 913efdf409e5
5
+Create Date: 2017-07-24 17:15:54.278141
6
+
7
+"""
8
+
9
+from alembic import op
10
+import sqlalchemy as sa
11
+
12
+# revision identifiers, used by Alembic.
13
+revision = 'f3852e1349c4'
14
+down_revision = '913efdf409e5'
15
+
16
+
17
+def upgrade():
18
+    """Drops the file content from revision."""
19
+    with op.batch_alter_table('content_revisions') as batch_op:
20
+        batch_op.drop_column('file_content')
21
+
22
+
23
+def downgrade():
24
+    """Adds the file content in revision."""
25
+    with op.batch_alter_table('content_revisions') as batch_op:
26
+        batch_op.add_column(sa.Column('file_content', sa.LargeBinary))

+ 6 - 1
tracim/tracim/config/app_cfg.py View File

129
 
129
 
130
 def configure_depot():
130
 def configure_depot():
131
     """Configure Depot."""
131
     """Configure Depot."""
132
-    depot_storage_name = 'default'
132
+    depot_storage_name = 'tracim'
133
     depot_storage_path = CFG.get_instance().DEPOT_STORAGE_DIR
133
     depot_storage_path = CFG.get_instance().DEPOT_STORAGE_DIR
134
     depot_storage_settings = {'depot.storage_path': depot_storage_path}
134
     depot_storage_settings = {'depot.storage_path': depot_storage_path}
135
     DepotManager.configure(
135
     DepotManager.configure(
214
         self.DEPOT_STORAGE_DIR = tg.config.get(
214
         self.DEPOT_STORAGE_DIR = tg.config.get(
215
             'depot_storage_dir',
215
             'depot_storage_dir',
216
         )
216
         )
217
+        if not self.DEPOT_STORAGE_DIR:
218
+            raise Exception(
219
+                'ERROR: depot_storage_dir configuration is mandatory. '
220
+                'Set it before continuing.'
221
+            )
217
         self.PREVIEW_CACHE_DIR = tg.config.get(
222
         self.PREVIEW_CACHE_DIR = tg.config.get(
218
             'preview_cache_dir',
223
             'preview_cache_dir',
219
         )
224
         )

+ 12 - 2
tracim/tracim/fixtures/content.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+from depot.io.utils import FileIntent
3
+
2
 from tracim import model
4
 from tracim import model
3
 from tracim.fixtures import Fixture
5
 from tracim.fixtures import Fixture
4
 from tracim.fixtures.users_and_groups import Test
6
 from tracim.fixtures.users_and_groups import Test
96
             do_save=False,
98
             do_save=False,
97
         )
99
         )
98
         w1f1d1_txt.file_extension = '.txt'
100
         w1f1d1_txt.file_extension = '.txt'
99
-        w1f1d1_txt.file_content = b'w1f1d1 content'
101
+        w1f1d1_txt.depot_file = FileIntent(
102
+            b'w1f1d1 content',
103
+            'w1f1d1.txt',
104
+            'text/plain',
105
+        )
100
         self._session.add(w1f1d1_txt)
106
         self._session.add(w1f1d1_txt)
101
         w1f1d2_html = content_api.create(
107
         w1f1d2_html = content_api.create(
102
             content_type=ContentType.File,
108
             content_type=ContentType.File,
106
             do_save=False,
112
             do_save=False,
107
         )
113
         )
108
         w1f1d2_html.file_extension = '.html'
114
         w1f1d2_html.file_extension = '.html'
109
-        w1f1d2_html.file_content = b'<p>w1f1d2 content</p>'
115
+        w1f1d2_html.depot_file = FileIntent(
116
+            b'<p>w1f1d2 content</p>',
117
+            'w1f1d2.html',
118
+            'text/html',
119
+        )
110
         self._session.add(w1f1d2_html)
120
         self._session.add(w1f1d2_html)
111
         w1f1f1 = content_api.create(
121
         w1f1f1 = content_api.create(
112
             content_type=ContentType.Folder,
122
             content_type=ContentType.Folder,

+ 7 - 3
tracim/tracim/lib/content.py View File

16
 from tg.i18n import ugettext as _
16
 from tg.i18n import ugettext as _
17
 
17
 
18
 from depot.manager import DepotManager
18
 from depot.manager import DepotManager
19
+from depot.io.utils import FileIntent
19
 
20
 
20
 import sqlalchemy
21
 import sqlalchemy
21
 from sqlalchemy.orm import aliased
22
 from sqlalchemy.orm import aliased
873
         item.revision_type = ActionDescription.EDITION
874
         item.revision_type = ActionDescription.EDITION
874
         return item
875
         return item
875
 
876
 
876
-    def update_file_data(self, item: Content, new_filename: str, new_mimetype: str, new_file_content) -> Content:
877
+    def update_file_data(self, item: Content, new_filename: str, new_mimetype: str, new_content: bytes) -> Content:
877
         item.owner = self._user
878
         item.owner = self._user
878
         item.file_name = new_filename
879
         item.file_name = new_filename
879
         item.file_mimetype = new_mimetype
880
         item.file_mimetype = new_mimetype
880
-        item.file_content = new_file_content
881
-        item.depot_file = new_file_content
881
+        item.depot_file = FileIntent(
882
+            new_content,
883
+            new_filename,
884
+            new_mimetype,
885
+        )
882
         item.revision_type = ActionDescription.REVISION
886
         item.revision_type = ActionDescription.REVISION
883
         return item
887
         return item
884
 
888
 

+ 6 - 5
tracim/tracim/lib/webdav/sql_resources.py View File

5
 
5
 
6
 import tg
6
 import tg
7
 import transaction
7
 import transaction
8
+import typing
8
 import re
9
 import re
9
 from datetime import datetime
10
 from datetime import datetime
10
 from time import mktime
11
 from time import mktime
883
         return "<DAVNonCollection: File (%d)>" % self.content.revision_id
884
         return "<DAVNonCollection: File (%d)>" % self.content.revision_id
884
 
885
 
885
     def getContentLength(self) -> int:
886
     def getContentLength(self) -> int:
886
-        return len(self.content.file_content)
887
+        return self.content.depot_file.file.content_length
887
 
888
 
888
     def getContentType(self) -> str:
889
     def getContentType(self) -> str:
889
         return self.content.file_mimetype
890
         return self.content.file_mimetype
897
     def getLastModified(self) -> float:
898
     def getLastModified(self) -> float:
898
         return mktime(self.content.updated.timetuple())
899
         return mktime(self.content.updated.timetuple())
899
 
900
 
900
-    def getContent(self):
901
+    def getContent(self) -> typing.BinaryIO:
901
         filestream = compat.BytesIO()
902
         filestream = compat.BytesIO()
902
-        filestream.write(self.content.file_content)
903
+        filestream.write(self.content.depot_file.file.read())
903
         filestream.seek(0)
904
         filestream.seek(0)
904
 
905
 
905
         return filestream
906
         return filestream
1028
 
1029
 
1029
     def getContent(self):
1030
     def getContent(self):
1030
         filestream = compat.BytesIO()
1031
         filestream = compat.BytesIO()
1031
-        filestream.write(self.content_revision.file_content)
1032
+        filestream.write(self.content_revision.depot_file.file.read())
1032
         filestream.seek(0)
1033
         filestream.seek(0)
1033
 
1034
 
1034
         return filestream
1035
         return filestream
1035
 
1036
 
1036
     def getContentLength(self):
1037
     def getContentLength(self):
1037
-        return len(self.content_revision.file_content)
1038
+        return self.content_revision.depot_file.file.content_length
1038
 
1039
 
1039
     def getContentType(self) -> str:
1040
     def getContentType(self) -> str:
1040
         return self.content_revision.file_mimetype
1041
         return self.content_revision.file_mimetype

+ 7 - 35
tracim/tracim/model/data.py View File

26
 from sqlalchemy.types import Unicode
26
 from sqlalchemy.types import Unicode
27
 from depot.fields.sqlalchemy import UploadedFileField
27
 from depot.fields.sqlalchemy import UploadedFileField
28
 from depot.fields.upload import UploadedFile
28
 from depot.fields.upload import UploadedFile
29
+from depot.io.utils import FileIntent
29
 
30
 
30
 from tracim.lib.utils import lazy_ugettext as l_
31
 from tracim.lib.utils import lazy_ugettext as l_
31
 from tracim.lib.exception import ContentRevisionUpdateError
32
 from tracim.lib.exception import ContentRevisionUpdateError
545
         server_default='',
546
         server_default='',
546
     )
547
     )
547
     file_mimetype = Column(Unicode(255),  unique=False, nullable=False, default='')
548
     file_mimetype = Column(Unicode(255),  unique=False, nullable=False, default='')
548
-    # TODO - A.P - 2017-07-03 - future removal planned
549
-    # file_content is to be replaced by depot_file, for now both coexist as
550
-    # this:
551
-    # - file_content data is still setted
552
-    # - newly created revision also gets depot_file data setted
553
-    # - access to the file of a revision from depot_file exclusively
554
-    # Here is the tasks workflow of the DB to OnDisk Switch :
555
-    # - Add depot_file "prototype style"
556
-    #   https://github.com/tracim/tracim/issues/233 - DONE
557
-    # - Integrate preview generator feature "prototype style"
558
-    #   https://github.com/tracim/tracim/issues/232 - DONE
559
-    # - Write migrations
560
-    #   https://github.com/tracim/tracim/issues/245
561
-    #   https://github.com/tracim/tracim/issues/246
562
-    # - Stabilize preview generator integration
563
-    #   includes dropping DB file content
564
-    #   https://github.com/tracim/tracim/issues/249
565
-    file_content = deferred(Column(LargeBinary(), unique=False, nullable=True))
566
     # INFO - A.P - 2017-07-03 - Depot Doc
549
     # INFO - A.P - 2017-07-03 - Depot Doc
567
     # http://depot.readthedocs.io/en/latest/#attaching-files-to-models
550
     # http://depot.readthedocs.io/en/latest/#attaching-files-to-models
568
     # http://depot.readthedocs.io/en/latest/api.html#module-depot.fields
551
     # http://depot.readthedocs.io/en/latest/api.html#module-depot.fields
592
         'content_id',
575
         'content_id',
593
         'created',
576
         'created',
594
         'description',
577
         'description',
595
-        'file_content',
596
         'file_mimetype',
578
         'file_mimetype',
597
         'file_extension',
579
         'file_extension',
598
         'is_archived',
580
         'is_archived',
649
             setattr(new_rev, column_name, column_value)
631
             setattr(new_rev, column_name, column_value)
650
 
632
 
651
         new_rev.updated = datetime.utcnow()
633
         new_rev.updated = datetime.utcnow()
652
-        # TODO APY tweaks here depot_file
653
-        # import pudb; pu.db
654
-        # new_rev.depot_file = DepotManager.get().get(revision.depot_file)
655
-        new_rev.depot_file = revision.file_content
634
+        if revision.depot_file:
635
+            new_rev.depot_file = FileIntent(
636
+                revision.depot_file.file.read(),
637
+                revision.file_name,
638
+                revision.file_mimetype,
639
+            )
656
 
640
 
657
         return new_rev
641
         return new_rev
658
 
642
 
861
         return ContentRevisionRO.file_mimetype
845
         return ContentRevisionRO.file_mimetype
862
 
846
 
863
     @hybrid_property
847
     @hybrid_property
864
-    def file_content(self):
865
-        return self.revision.file_content
866
-
867
-    @file_content.setter
868
-    def file_content(self, value):
869
-        self.revision.file_content = value
870
-
871
-    @file_content.expression
872
-    def file_content(cls) -> InstrumentedAttribute:
873
-        return ContentRevisionRO.file_content
874
-
875
-    @hybrid_property
876
     def _properties(self) -> str:
848
     def _properties(self) -> str:
877
         return self.revision.properties
849
         return self.revision.properties
878
 
850
 

+ 1 - 1
tracim/tracim/tests/library/test_content_api.py View File

631
                                                           updated.owner_id))
631
                                                           updated.owner_id))
632
         eq_('this_is_a_page.html', updated.file_name)
632
         eq_('this_is_a_page.html', updated.file_name)
633
         eq_('text/html', updated.file_mimetype)
633
         eq_('text/html', updated.file_mimetype)
634
-        eq_(b'<html>hello world</html>', updated.file_content)
634
+        eq_(b'<html>hello world</html>', updated.depot_file.file.read())
635
         eq_(ActionDescription.REVISION, updated.revision_type)
635
         eq_(ActionDescription.REVISION, updated.revision_type)
636
 
636
 
637
     def test_archive_unarchive(self):
637
     def test_archive_unarchive(self):

+ 4 - 4
tracim/tracim/tests/library/test_webdav.py View File

307
         ))
307
         ))
308
         eq_(
308
         eq_(
309
             b'hello\n',
309
             b'hello\n',
310
-            result.content.file_content,
310
+            result.content.depot_file.file.read(),
311
             msg='fiel content should be "hello\n" but it is {0}'.format(
311
             msg='fiel content should be "hello\n" but it is {0}'.format(
312
-                result.content.file_content
312
+                result.content.depot_file.file.read()
313
             )
313
             )
314
         )
314
         )
315
 
315
 
550
         ))
550
         ))
551
         eq_(
551
         eq_(
552
             b'hello\n',
552
             b'hello\n',
553
-            result.content.file_content,
553
+            result.content.depot_file.file.read(),
554
             msg='fiel content should be "hello\n" but it is {0}'.format(
554
             msg='fiel content should be "hello\n" but it is {0}'.format(
555
-                result.content.file_content
555
+                result.content.depot_file.file.read()
556
             )
556
             )
557
         )
557
         )
558
 
558
 

+ 1 - 1
tracim/tracim/tests/models/test_content_revision.py View File

13
 class TestContentRevision(BaseTest, TestStandard):
13
 class TestContentRevision(BaseTest, TestStandard):
14
 
14
 
15
     def _new_from(self, revision):
15
     def _new_from(self, revision):
16
-        excluded_columns = ('revision_id', '_sa_instance_state')
16
+        excluded_columns = ('revision_id', '_sa_instance_state', 'depot_file')
17
         revision_columns = [attr.key for attr in inspect(revision).attrs if not attr.key in excluded_columns]
17
         revision_columns = [attr.key for attr in inspect(revision).attrs if not attr.key in excluded_columns]
18
         new_revision = ContentRevisionRO()
18
         new_revision = ContentRevisionRO()
19
 
19
 

+ 0 - 0
tracim/wsgidav.conf.sample View File