1import time
2import os
3
4from Db.Db import Db, DbTableError
5from Config import config
6from Plugin import PluginManager
7from Debug import Debug
8
9
10@PluginManager.acceptPlugins
11class ContentDb(Db):
12    def __init__(self, path):
13        Db.__init__(self, {"db_name": "ContentDb", "tables": {}}, path)
14        self.foreign_keys = True
15        try:
16            self.schema = self.getSchema()
17            try:
18                self.checkTables()
19            except DbTableError:
20                pass
21            self.log.debug("Checking foreign keys...")
22            foreign_key_error = self.execute("PRAGMA foreign_key_check").fetchone()
23            if foreign_key_error:
24                raise Exception("Database foreign key error: %s" % foreign_key_error)
25        except Exception as err:
26            self.log.error("Error loading content.db: %s, rebuilding..." % Debug.formatException(err))
27            self.close()
28            os.unlink(path)  # Remove and try again
29            Db.__init__(self, {"db_name": "ContentDb", "tables": {}}, path)
30            self.foreign_keys = True
31            self.schema = self.getSchema()
32            try:
33                self.checkTables()
34            except DbTableError:
35                pass
36        self.site_ids = {}
37        self.sites = {}
38
39    def getSchema(self):
40        schema = {}
41        schema["db_name"] = "ContentDb"
42        schema["version"] = 3
43        schema["tables"] = {}
44
45        if not self.getTableVersion("site"):
46            self.log.debug("Migrating from table version-less content.db")
47            version = int(self.execute("PRAGMA user_version").fetchone()[0])
48            if version > 0:
49                self.checkTables()
50                self.execute("INSERT INTO keyvalue ?", {"json_id": 0, "key": "table.site.version", "value": 1})
51                self.execute("INSERT INTO keyvalue ?", {"json_id": 0, "key": "table.content.version", "value": 1})
52
53        schema["tables"]["site"] = {
54            "cols": [
55                ["site_id", "INTEGER  PRIMARY KEY ASC NOT NULL UNIQUE"],
56                ["address", "TEXT NOT NULL"]
57            ],
58            "indexes": [
59                "CREATE UNIQUE INDEX site_address ON site (address)"
60            ],
61            "schema_changed": 1
62        }
63
64        schema["tables"]["content"] = {
65            "cols": [
66                ["content_id", "INTEGER PRIMARY KEY UNIQUE NOT NULL"],
67                ["site_id", "INTEGER REFERENCES site (site_id) ON DELETE CASCADE"],
68                ["inner_path", "TEXT"],
69                ["size", "INTEGER"],
70                ["size_files", "INTEGER"],
71                ["size_files_optional", "INTEGER"],
72                ["modified", "INTEGER"]
73            ],
74            "indexes": [
75                "CREATE UNIQUE INDEX content_key ON content (site_id, inner_path)",
76                "CREATE INDEX content_modified ON content (site_id, modified)"
77            ],
78            "schema_changed": 1
79        }
80
81        return schema
82
83    def initSite(self, site):
84        self.sites[site.address] = site
85
86    def needSite(self, site):
87        if site.address not in self.site_ids:
88            self.execute("INSERT OR IGNORE INTO site ?", {"address": site.address})
89            self.site_ids = {}
90            for row in self.execute("SELECT * FROM site"):
91                self.site_ids[row["address"]] = row["site_id"]
92        return self.site_ids[site.address]
93
94    def deleteSite(self, site):
95        site_id = self.site_ids.get(site.address, 0)
96        if site_id:
97            self.execute("DELETE FROM site WHERE site_id = :site_id", {"site_id": site_id})
98            del self.site_ids[site.address]
99            del self.sites[site.address]
100
101    def setContent(self, site, inner_path, content, size=0):
102        self.insertOrUpdate("content", {
103            "size": size,
104            "size_files": sum([val["size"] for key, val in content.get("files", {}).items()]),
105            "size_files_optional": sum([val["size"] for key, val in content.get("files_optional", {}).items()]),
106            "modified": int(content.get("modified", 0))
107        }, {
108            "site_id": self.site_ids.get(site.address, 0),
109            "inner_path": inner_path
110        })
111
112    def deleteContent(self, site, inner_path):
113        self.execute("DELETE FROM content WHERE ?", {"site_id": self.site_ids.get(site.address, 0), "inner_path": inner_path})
114
115    def loadDbDict(self, site):
116        res = self.execute(
117            "SELECT GROUP_CONCAT(inner_path, '|') AS inner_paths FROM content WHERE ?",
118            {"site_id": self.site_ids.get(site.address, 0)}
119        )
120        row = res.fetchone()
121        if row and row["inner_paths"]:
122            inner_paths = row["inner_paths"].split("|")
123            return dict.fromkeys(inner_paths, False)
124        else:
125            return {}
126
127    def getTotalSize(self, site, ignore=None):
128        params = {"site_id": self.site_ids.get(site.address, 0)}
129        if ignore:
130            params["not__inner_path"] = ignore
131        res = self.execute("SELECT SUM(size) + SUM(size_files) AS size, SUM(size_files_optional) AS size_optional FROM content WHERE ?", params)
132        row = dict(res.fetchone())
133
134        if not row["size"]:
135            row["size"] = 0
136        if not row["size_optional"]:
137            row["size_optional"] = 0
138
139        return row["size"], row["size_optional"]
140
141    def listModified(self, site, after=None, before=None):
142        params = {"site_id": self.site_ids.get(site.address, 0)}
143        if after:
144            params["modified>"] = after
145        if before:
146            params["modified<"] = before
147        res = self.execute("SELECT inner_path, modified FROM content WHERE ?", params)
148        return {row["inner_path"]: row["modified"] for row in res}
149
150content_dbs = {}
151
152
153def getContentDb(path=None):
154    if not path:
155        path = "%s/content.db" % config.data_dir
156    if path not in content_dbs:
157        content_dbs[path] = ContentDb(path)
158    return content_dbs[path]
159
160getContentDb()  # Pre-connect to default one
161