浏览代码

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

Tracim 7 年前
父节点
当前提交
56cf2f4df8

+ 34 - 0
doc/migration.md 查看文件

@@ -0,0 +1,34 @@
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 查看文件

@@ -1,4 +1,4 @@
1
-"""files on disk
1
+"""files on disk.
2 2
 
3 3
 Revision ID: 69fb10c3d6f0
4 4
 Revises: c1cea4bbae16
@@ -15,11 +15,24 @@ revision = '69fb10c3d6f0'
15 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 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 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 查看文件

@@ -0,0 +1,87 @@
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 查看文件

@@ -0,0 +1,26 @@
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 查看文件

@@ -129,7 +129,7 @@ def start_daemons(manager: DaemonsManager):
129 129
 
130 130
 def configure_depot():
131 131
     """Configure Depot."""
132
-    depot_storage_name = 'default'
132
+    depot_storage_name = 'tracim'
133 133
     depot_storage_path = CFG.get_instance().DEPOT_STORAGE_DIR
134 134
     depot_storage_settings = {'depot.storage_path': depot_storage_path}
135 135
     DepotManager.configure(
@@ -214,6 +214,11 @@ class CFG(object):
214 214
         self.DEPOT_STORAGE_DIR = tg.config.get(
215 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 222
         self.PREVIEW_CACHE_DIR = tg.config.get(
218 223
             'preview_cache_dir',
219 224
         )

+ 12 - 2
tracim/tracim/fixtures/content.py 查看文件

@@ -1,4 +1,6 @@
1 1
 # -*- coding: utf-8 -*-
2
+from depot.io.utils import FileIntent
3
+
2 4
 from tracim import model
3 5
 from tracim.fixtures import Fixture
4 6
 from tracim.fixtures.users_and_groups import Test
@@ -96,7 +98,11 @@ class Content(Fixture):
96 98
             do_save=False,
97 99
         )
98 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 106
         self._session.add(w1f1d1_txt)
101 107
         w1f1d2_html = content_api.create(
102 108
             content_type=ContentType.File,
@@ -106,7 +112,11 @@ class Content(Fixture):
106 112
             do_save=False,
107 113
         )
108 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 120
         self._session.add(w1f1d2_html)
111 121
         w1f1f1 = content_api.create(
112 122
             content_type=ContentType.Folder,

+ 7 - 3
tracim/tracim/lib/content.py 查看文件

@@ -16,6 +16,7 @@ import tg
16 16
 from tg.i18n import ugettext as _
17 17
 
18 18
 from depot.manager import DepotManager
19
+from depot.io.utils import FileIntent
19 20
 
20 21
 import sqlalchemy
21 22
 from sqlalchemy.orm import aliased
@@ -873,12 +874,15 @@ class ContentApi(object):
873 874
         item.revision_type = ActionDescription.EDITION
874 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 878
         item.owner = self._user
878 879
         item.file_name = new_filename
879 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 886
         item.revision_type = ActionDescription.REVISION
883 887
         return item
884 888
 

+ 6 - 5
tracim/tracim/lib/webdav/sql_resources.py 查看文件

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

+ 7 - 35
tracim/tracim/model/data.py 查看文件

@@ -26,6 +26,7 @@ from sqlalchemy.types import Text
26 26
 from sqlalchemy.types import Unicode
27 27
 from depot.fields.sqlalchemy import UploadedFileField
28 28
 from depot.fields.upload import UploadedFile
29
+from depot.io.utils import FileIntent
29 30
 
30 31
 from tracim.lib.utils import lazy_ugettext as l_
31 32
 from tracim.lib.exception import ContentRevisionUpdateError
@@ -545,24 +546,6 @@ class ContentRevisionRO(DeclarativeBase):
545 546
         server_default='',
546 547
     )
547 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 549
     # INFO - A.P - 2017-07-03 - Depot Doc
567 550
     # http://depot.readthedocs.io/en/latest/#attaching-files-to-models
568 551
     # http://depot.readthedocs.io/en/latest/api.html#module-depot.fields
@@ -592,7 +575,6 @@ class ContentRevisionRO(DeclarativeBase):
592 575
         'content_id',
593 576
         'created',
594 577
         'description',
595
-        'file_content',
596 578
         'file_mimetype',
597 579
         'file_extension',
598 580
         'is_archived',
@@ -649,10 +631,12 @@ class ContentRevisionRO(DeclarativeBase):
649 631
             setattr(new_rev, column_name, column_value)
650 632
 
651 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 641
         return new_rev
658 642
 
@@ -861,18 +845,6 @@ class Content(DeclarativeBase):
861 845
         return ContentRevisionRO.file_mimetype
862 846
 
863 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 848
     def _properties(self) -> str:
877 849
         return self.revision.properties
878 850
 

+ 1 - 1
tracim/tracim/tests/library/test_content_api.py 查看文件

@@ -631,7 +631,7 @@ class TestContentApi(BaseTest, TestStandard):
631 631
                                                           updated.owner_id))
632 632
         eq_('this_is_a_page.html', updated.file_name)
633 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 635
         eq_(ActionDescription.REVISION, updated.revision_type)
636 636
 
637 637
     def test_archive_unarchive(self):

+ 4 - 4
tracim/tracim/tests/library/test_webdav.py 查看文件

@@ -307,9 +307,9 @@ class TestWebDav(TestStandard):
307 307
         ))
308 308
         eq_(
309 309
             b'hello\n',
310
-            result.content.file_content,
310
+            result.content.depot_file.file.read(),
311 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,9 +550,9 @@ class TestWebDav(TestStandard):
550 550
         ))
551 551
         eq_(
552 552
             b'hello\n',
553
-            result.content.file_content,
553
+            result.content.depot_file.file.read(),
554 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 查看文件

@@ -13,7 +13,7 @@ from tracim.tests import TestStandard, BaseTest
13 13
 class TestContentRevision(BaseTest, TestStandard):
14 14
 
15 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 17
         revision_columns = [attr.key for attr in inspect(revision).attrs if not attr.key in excluded_columns]
18 18
         new_revision = ContentRevisionRO()
19 19
 

+ 0 - 0
tracim/wsgidav.conf.sample 查看文件