1import os
2import re
3from .p3 import urlparse, u_maybe
4
5from .content import (check_file, check_directory, read_text_file, write_file,
6                      system_path, check_content, copy_content)
7
8from . import content
9
10
11META_EXT = '.yaml'
12BIB_EXT  = '.bib'
13
14
15def filter_filename(filename, ext):
16    """ Return the filename without the extension if the extension matches ext.
17        Otherwise return None
18    """
19    pattern = '.*\{}$'.format(ext)
20    if re.match(pattern, filename) is not None:
21        return u_maybe(filename[:-len(ext)])
22
23
24class FileBroker(object):
25    """ Handles all access to meta and bib files of the repository.
26
27        * Does *absolutely no* encoding/decoding.
28        * Communicate failure with exceptions.
29    """
30
31    def __init__(self, directory, create=False):
32        self.directory = os.path.expanduser(directory)
33        self.metadir   = os.path.join(self.directory, 'meta')
34        self.bibdir    = os.path.join(self.directory, 'bib')
35        self.cachedir  = os.path.join(self.directory, '.cache')
36        if create:
37            self._create()
38        check_directory(self.directory)
39        check_directory(self.metadir)
40        check_directory(self.bibdir)
41        # cache directory is created (if absent) if other directories exists.
42        if not check_directory(self.cachedir, fail=False):
43            os.mkdir(system_path(self.cachedir))
44
45    def _create(self):
46        """Create meta and bib directories if absent"""
47        if not check_directory(self.directory, fail=False):
48            os.mkdir(system_path(self.directory))
49        if not check_directory(self.metadir, fail=False):
50            os.mkdir(system_path(self.metadir))
51        if not check_directory(self.bibdir, fail=False):
52            os.mkdir(system_path(self.bibdir))
53
54    def bib_path(self, citekey):
55        return os.path.join(self.bibdir, citekey + BIB_EXT)
56
57    def meta_path(self, citekey):
58        return os.path.join(self.metadir, citekey + META_EXT)
59
60    def pull_cachefile(self, filename):
61        filepath = os.path.join(self.cachedir, filename)
62        return content.read_binary_file(filepath)
63
64    def push_cachefile(self, filename, data):
65        filepath = os.path.join(self.cachedir, filename)
66        write_file(filepath, data, mode='wb')
67
68    def mtime_metafile(self, citekey):
69        try:
70            filepath = self.meta_path(citekey)
71            return os.path.getmtime(filepath)
72        except OSError:
73            raise IOError("'{}' not found.".format(filepath))
74
75    def mtime_bibfile(self, citekey):
76        try:
77            filepath = self.bib_path(citekey)
78            return os.path.getmtime(filepath)
79        except OSError:
80            raise IOError("'{}' not found.".format(filepath))
81
82    def pull_metafile(self, citekey):
83        return read_text_file(self.meta_path(citekey))
84
85    def pull_bibfile(self, citekey):
86        return read_text_file(self.bib_path(citekey))
87
88    def push_metafile(self, citekey, metadata):
89        """Put content to disk. Will gladly override anything standing in its way."""
90        write_file(self.meta_path(citekey), metadata)
91
92    def push_bibfile(self, citekey, bibdata):
93        """Put content to disk. Will gladly override anything standing in its way."""
94        write_file(self.bib_path(citekey), bibdata)
95
96    def push(self, citekey, metadata, bibdata):
97        """Put content to disk. Will gladly override anything standing in its way."""
98        self.push_metafile(citekey, metadata)
99        self.push_bibfile(citekey, bibdata)
100
101    def remove(self, citekey):
102        metafilepath = self.meta_path(citekey)
103        if check_file(metafilepath):
104            os.remove(system_path(metafilepath))
105        bibfilepath = self.bib_path(citekey)
106        if check_file(bibfilepath):
107            os.remove(system_path(bibfilepath))
108
109    def exists(self, citekey, meta_check=False):
110        """ Checks wether the bibtex of a citekey exists.
111
112            :param meta_check:  if True, will return if both the bibtex and the meta file exists.
113        """
114        does_exists = check_file(self.bib_path(citekey), fail=False)
115        if meta_check:
116            meta_exists = check_file(self.meta_path(citekey), fail=False)
117            does_exists = does_exists and meta_exists
118        return does_exists
119
120    def listing(self, filestats=True):
121        metafiles = []
122        for filename in os.listdir(system_path(self.metadir)):
123            citekey = filter_filename(filename, META_EXT)
124            if citekey is not None:
125                if filestats:
126                    stats = os.stat(system_path(os.path.join(self.metadir, filename)))
127                    metafiles.append(citekey, stats)
128                else:
129                    metafiles.append(citekey)
130
131        bibfiles = []
132        for filename in os.listdir(system_path(self.bibdir)):
133            citekey = filter_filename(filename, BIB_EXT)
134            if citekey is not None:
135                if filestats:
136                    stats = os.stat(system_path(os.path.join(self.bibdir, filename)))
137                    bibfiles.append(citekey, stats)
138                else:
139                    bibfiles.append(citekey)
140
141        return {'metafiles': metafiles, 'bibfiles': bibfiles}
142
143
144class DocBroker(object):
145    """ DocBroker manages the document files optionally attached to the papers.
146
147        * only one document can be attached to a paper (might change in the future)
148        * this document can be anything, the content is never processed.
149        * these document have an adress of the type "docsdir://citekey.pdf"
150        * docsdir:// correspond to /path/to/pubsdir/doc (configurable)
151        * document outside of the repository will not be removed.
152        * move_doc only applies from inside to inside the docsdir
153    """
154
155    def __init__(self, directory, scheme='docsdir', subdir='doc'):
156        self.scheme = scheme
157        self.docdir = os.path.join(directory, subdir)
158        if not check_directory(self.docdir, fail=False):
159            os.mkdir(system_path(self.docdir))
160
161    def in_docsdir(self, docpath):
162        try:
163            parsed = urlparse(docpath)
164        except Exception:
165            return False
166        return parsed.scheme == self.scheme
167
168    # def doc_exists(self, citekey, ext='.txt'):
169    #     return check_file(os.path.join(self.docdir, citekey + ext), fail=False)
170
171    def real_docpath(self, docpath):
172        """ Return the full path
173            Essentially transform pubsdir://doc/{citekey}.{ext} to /path/to/pubsdir/doc/{citekey}.{ext}.
174            Return absoluted paths of regular ones otherwise.
175        """
176        if self.in_docsdir(docpath):
177            parsed = urlparse(docpath)
178            if parsed.path == '':
179                docpath = os.path.join(self.docdir, parsed.netloc)
180            else:
181                docpath = os.path.join(self.docdir, parsed.netloc, parsed.path[1:])
182        return docpath
183
184    def add_doc(self, citekey, source_path, overwrite=False):
185        """ Add a document to the docsdir, and return its location.
186
187            The document will be named {citekey}.{ext}.
188            The location will be docsdir://{citekey}.{ext}.
189            :param overwrite: will overwrite existing file.
190            :return: the above location
191        """
192        full_source_path = self.real_docpath(source_path)
193        check_content(full_source_path)
194
195        target_path = '{}://{}'.format(self.scheme, citekey + os.path.splitext(source_path)[-1])
196        full_target_path = self.real_docpath(target_path)
197        copy_content(full_source_path, full_target_path, overwrite=overwrite)
198        return target_path
199
200    def remove_doc(self, docpath, silent=True):
201        """ Will remove only file hosted in docsdir://
202
203            :raise ValueError: for other paths, unless :param silent: is True
204        """
205        if not self.in_docsdir(docpath):
206            if not silent:
207                raise ValueError(('the file to be removed {} is set as external. '
208                                  'you should remove it manually.').format(docpath))
209            return
210        filepath = self.real_docpath(docpath)
211        if check_file(filepath):
212            os.remove(system_path(filepath))
213
214    def rename_doc(self, docpath, new_citekey):
215        """ Move a document inside the docsdir
216
217            :raise IOError: if docpath doesn't point to a file
218                            if new_citekey doc exists already.
219            :raise ValueError: if docpath is not in docsdir().
220
221            if an exception is raised, the files on disk haven't changed.
222        """
223        if not self.in_docsdir(docpath):
224            raise ValueError('cannot rename an external file ({}).'.format(docpath))
225
226        new_docpath = self.add_doc(new_citekey, docpath)
227        self.remove_doc(docpath)
228
229        return new_docpath
230