Statistics
| Revision:

gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / dulwich / contrib / swift.py @ 959

History | View | Annotate | Download (34.5 KB)

1
# swift.py -- Repo implementation atop OpenStack SWIFT
2
# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
3
#
4
# Author: Fabien Boucher <fabien.boucher@enovance.com>
5
#
6
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
7
# General Public License as public by the Free Software Foundation; version 2.0
8
# or (at your option) any later version. You can redistribute it and/or
9
# modify it under the terms of either of these two licenses.
10
#
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS,
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
# See the License for the specific language governing permissions and
15
# limitations under the License.
16
#
17
# You should have received a copy of the licenses; if not, see
18
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
19
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
20
# License, Version 2.0.
21
#
22

    
23
"""Repo implementation atop OpenStack SWIFT."""
24

    
25
# TODO: Refactor to share more code with dulwich/repo.py.
26
# TODO(fbo): Second attempt to _send() must be notified via real log
27
# TODO(fbo): More logs for operations
28

    
29
import os
30
import stat
31
import zlib
32
import tempfile
33
import posixpath
34

    
35
try:
36
    import urlparse
37
except ImportError:
38
    import urllib.parse as urlparse
39

    
40
from io import BytesIO
41
try:
42
    from ConfigParser import ConfigParser
43
except ImportError:
44
    from configparser import ConfigParser
45
from geventhttpclient import HTTPClient
46

    
47
from dulwich.greenthreads import (
48
    GreenThreadsMissingObjectFinder,
49
    GreenThreadsObjectStoreIterator,
50
    )
51

    
52
from dulwich.lru_cache import LRUSizeCache
53
from dulwich.objects import (
54
    Blob,
55
    Commit,
56
    Tree,
57
    Tag,
58
    S_ISGITLINK,
59
    )
60
from dulwich.object_store import (
61
    PackBasedObjectStore,
62
    PACKDIR,
63
    INFODIR,
64
    )
65
from dulwich.pack import (
66
    PackData,
67
    Pack,
68
    PackIndexer,
69
    PackStreamCopier,
70
    write_pack_header,
71
    compute_file_sha,
72
    iter_sha1,
73
    write_pack_index_v2,
74
    load_pack_index_file,
75
    read_pack_header,
76
    _compute_object_size,
77
    unpack_object,
78
    write_pack_object,
79
    )
80
from dulwich.protocol import TCP_GIT_PORT
81
from dulwich.refs import (
82
    InfoRefsContainer,
83
    read_info_refs,
84
    write_info_refs,
85
    )
86
from dulwich.repo import (
87
    BaseRepo,
88
    OBJECTDIR,
89
    )
90
from dulwich.server import (
91
    Backend,
92
    TCPGitServer,
93
    )
94

    
95
try:
96
    from simplejson import loads as json_loads
97
    from simplejson import dumps as json_dumps
98
except ImportError:
99
    from json import loads as json_loads
100
    from json import dumps as json_dumps
101

    
102
import sys
103

    
104

    
105
"""
106
# Configuration file sample
107
[swift]
108
# Authentication URL (Keystone or Swift)
109
auth_url = http://127.0.0.1:5000/v2.0
110
# Authentication version to use
111
auth_ver = 2
112
# The tenant and username separated by a semicolon
113
username = admin;admin
114
# The user password
115
password = pass
116
# The Object storage region to use (auth v2) (Default RegionOne)
117
region_name = RegionOne
118
# The Object storage endpoint URL to use (auth v2) (Default internalURL)
119
endpoint_type = internalURL
120
# Concurrency to use for parallel tasks (Default 10)
121
concurrency = 10
122
# Size of the HTTP pool (Default 10)
123
http_pool_length = 10
124
# Timeout delay for HTTP connections (Default 20)
125
http_timeout = 20
126
# Chunk size to read from pack (Bytes) (Default 12228)
127
chunk_length = 12228
128
# Cache size (MBytes) (Default 20)
129
cache_length = 20
130
"""
131

    
132

    
133
class PackInfoObjectStoreIterator(GreenThreadsObjectStoreIterator):
134

    
135
    def __len__(self):
136
        while len(self.finder.objects_to_send):
137
            for _ in range(0, len(self.finder.objects_to_send)):
138
                sha = self.finder.next()
139
                self._shas.append(sha)
140
        return len(self._shas)
141

    
142

    
143
class PackInfoMissingObjectFinder(GreenThreadsMissingObjectFinder):
144

    
145
    def next(self):
146
        while True:
147
            if not self.objects_to_send:
148
                return None
149
            (sha, name, leaf) = self.objects_to_send.pop()
150
            if sha not in self.sha_done:
151
                break
152
        if not leaf:
153
            info = self.object_store.pack_info_get(sha)
154
            if info[0] == Commit.type_num:
155
                self.add_todo([(info[2], "", False)])
156
            elif info[0] == Tree.type_num:
157
                self.add_todo([tuple(i) for i in info[1]])
158
            elif info[0] == Tag.type_num:
159
                self.add_todo([(info[1], None, False)])
160
            if sha in self._tagged:
161
                self.add_todo([(self._tagged[sha], None, True)])
162
        self.sha_done.add(sha)
163
        self.progress("counting objects: %d\r" % len(self.sha_done))
164
        return (sha, name)
165

    
166

    
167
def load_conf(path=None, file=None):
168
    """Load configuration in global var CONF
169

170
    :param path: The path to the configuration file
171
    :param file: If provided read instead the file like object
172
    """
173
    conf = ConfigParser()
174
    if file:
175
        conf.readfp(file)
176
        return conf
177
    confpath = None
178
    if not path:
179
        try:
180
            confpath = os.environ['DULWICH_SWIFT_CFG']
181
        except KeyError:
182
            raise Exception("You need to specify a configuration file")
183
    else:
184
        confpath = path
185
    if not os.path.isfile(confpath):
186
        raise Exception("Unable to read configuration file %s" % confpath)
187
    conf.read(confpath)
188
    return conf
189

    
190

    
191
def swift_load_pack_index(scon, filename):
192
    """Read a pack index file from Swift
193

194
    :param scon: a `SwiftConnector` instance
195
    :param filename: Path to the index file objectise
196
    :return: a `PackIndexer` instance
197
    """
198
    f = scon.get_object(filename)
199
    try:
200
        return load_pack_index_file(filename, f)
201
    finally:
202
        f.close()
203

    
204

    
205
def pack_info_create(pack_data, pack_index):
206
    pack = Pack.from_objects(pack_data, pack_index)
207
    info = {}
208
    for obj in pack.iterobjects():
209
        # Commit
210
        if obj.type_num == Commit.type_num:
211
            info[obj.id] = (obj.type_num, obj.parents, obj.tree)
212
        # Tree
213
        elif obj.type_num == Tree.type_num:
214
            shas = [(s, n, not stat.S_ISDIR(m)) for
215
                    n, m, s in obj.items() if not S_ISGITLINK(m)]
216
            info[obj.id] = (obj.type_num, shas)
217
        # Blob
218
        elif obj.type_num == Blob.type_num:
219
            info[obj.id] = None
220
        # Tag
221
        elif obj.type_num == Tag.type_num:
222
            info[obj.id] = (obj.type_num, obj.object[1])
223
    return zlib.compress(json_dumps(info))
224

    
225

    
226
def load_pack_info(filename, scon=None, file=None):
227
    if not file:
228
        f = scon.get_object(filename)
229
    else:
230
        f = file
231
    if not f:
232
        return None
233
    try:
234
        return json_loads(zlib.decompress(f.read()))
235
    finally:
236
        f.close()
237

    
238

    
239
class SwiftException(Exception):
240
    pass
241

    
242

    
243
class SwiftConnector(object):
244
    """A Connector to swift that manage authentication and errors catching
245
    """
246

    
247
    def __init__(self, root, conf):
248
        """ Initialize a SwiftConnector
249

250
        :param root: The swift container that will act as Git bare repository
251
        :param conf: A ConfigParser Object
252
        """
253
        self.conf = conf
254
        self.auth_ver = self.conf.get("swift", "auth_ver")
255
        if self.auth_ver not in ["1", "2"]:
256
            raise NotImplementedError(
257
                "Wrong authentication version use either 1 or 2")
258
        self.auth_url = self.conf.get("swift", "auth_url")
259
        self.user = self.conf.get("swift", "username")
260
        self.password = self.conf.get("swift", "password")
261
        self.concurrency = self.conf.getint('swift', 'concurrency') or 10
262
        self.http_timeout = self.conf.getint('swift', 'http_timeout') or 20
263
        self.http_pool_length = \
264
            self.conf.getint('swift', 'http_pool_length') or 10
265
        self.region_name = self.conf.get("swift", "region_name") or "RegionOne"
266
        self.endpoint_type = \
267
            self.conf.get("swift", "endpoint_type") or "internalURL"
268
        self.cache_length = self.conf.getint("swift", "cache_length") or 20
269
        self.chunk_length = self.conf.getint("swift", "chunk_length") or 12228
270
        self.root = root
271
        block_size = 1024 * 12  # 12KB
272
        if self.auth_ver == "1":
273
            self.storage_url, self.token = self.swift_auth_v1()
274
        else:
275
            self.storage_url, self.token = self.swift_auth_v2()
276

    
277
        token_header = {'X-Auth-Token': str(self.token)}
278
        self.httpclient = \
279
            HTTPClient.from_url(str(self.storage_url),
280
                                concurrency=self.http_pool_length,
281
                                block_size=block_size,
282
                                connection_timeout=self.http_timeout,
283
                                network_timeout=self.http_timeout,
284
                                headers=token_header)
285
        self.base_path = str(
286
            posixpath.join(urlparse.urlparse(self.storage_url).path, self.root))
287

    
288
    def swift_auth_v1(self):
289
        self.user = self.user.replace(";", ":")
290
        auth_httpclient = HTTPClient.from_url(
291
            self.auth_url,
292
            connection_timeout=self.http_timeout,
293
            network_timeout=self.http_timeout,
294
            )
295
        headers = {'X-Auth-User': self.user,
296
                   'X-Auth-Key': self.password}
297
        path = urlparse.urlparse(self.auth_url).path
298

    
299
        ret = auth_httpclient.request('GET', path, headers=headers)
300

    
301
        # Should do something with redirections (301 in my case)
302

    
303
        if ret.status_code < 200 or ret.status_code >= 300:
304
            raise SwiftException('AUTH v1.0 request failed on ' +
305
                                 '%s with error code %s (%s)'
306
                                 % (str(auth_httpclient.get_base_url()) +
307
                                    path, ret.status_code,
308
                                    str(ret.items())))
309
        storage_url = ret['X-Storage-Url']
310
        token = ret['X-Auth-Token']
311
        return storage_url, token
312

    
313
    def swift_auth_v2(self):
314
        self.tenant, self.user = self.user.split(';')
315
        auth_dict = {}
316
        auth_dict['auth'] = {'passwordCredentials':
317
                             {
318
                                 'username': self.user,
319
                                 'password': self.password,
320
                             },
321
                             'tenantName': self.tenant}
322
        auth_json = json_dumps(auth_dict)
323
        headers = {'Content-Type': 'application/json'}
324
        auth_httpclient = HTTPClient.from_url(
325
            self.auth_url,
326
            connection_timeout=self.http_timeout,
327
            network_timeout=self.http_timeout,
328
            )
329
        path = urlparse.urlparse(self.auth_url).path
330
        if not path.endswith('tokens'):
331
            path = posixpath.join(path, 'tokens')
332
        ret = auth_httpclient.request('POST', path,
333
                                      body=auth_json,
334
                                      headers=headers)
335

    
336
        if ret.status_code < 200 or ret.status_code >= 300:
337
            raise SwiftException('AUTH v2.0 request failed on ' +
338
                                 '%s with error code %s (%s)'
339
                                 % (str(auth_httpclient.get_base_url()) +
340
                                    path, ret.status_code,
341
                                    str(ret.items())))
342
        auth_ret_json = json_loads(ret.read())
343
        token = auth_ret_json['access']['token']['id']
344
        catalogs = auth_ret_json['access']['serviceCatalog']
345
        object_store = [o_store for o_store in catalogs if
346
                        o_store['type'] == 'object-store'][0]
347
        endpoints = object_store['endpoints']
348
        endpoint = [endp for endp in endpoints if
349
                    endp["region"] == self.region_name][0]
350
        return endpoint[self.endpoint_type], token
351

    
352
    def test_root_exists(self):
353
        """Check that Swift container exist
354

355
        :return: True if exist or None it not
356
        """
357
        ret = self.httpclient.request('HEAD', self.base_path)
358
        if ret.status_code == 404:
359
            return None
360
        if ret.status_code < 200 or ret.status_code > 300:
361
            raise SwiftException('HEAD request failed with error code %s'
362
                                 % ret.status_code)
363
        return True
364

    
365
    def create_root(self):
366
        """Create the Swift container
367

368
        :raise: `SwiftException` if unable to create
369
        """
370
        if not self.test_root_exists():
371
            ret = self.httpclient.request('PUT', self.base_path)
372
            if ret.status_code < 200 or ret.status_code > 300:
373
                raise SwiftException('PUT request failed with error code %s'
374
                                     % ret.status_code)
375

    
376
    def get_container_objects(self):
377
        """Retrieve objects list in a container
378

379
        :return: A list of dict that describe objects
380
                 or None if container does not exist
381
        """
382
        qs = '?format=json'
383
        path = self.base_path + qs
384
        ret = self.httpclient.request('GET', path)
385
        if ret.status_code == 404:
386
            return None
387
        if ret.status_code < 200 or ret.status_code > 300:
388
            raise SwiftException('GET request failed with error code %s'
389
                                 % ret.status_code)
390
        content = ret.read()
391
        return json_loads(content)
392

    
393
    def get_object_stat(self, name):
394
        """Retrieve object stat
395

396
        :param name: The object name
397
        :return: A dict that describe the object
398
                 or None if object does not exist
399
        """
400
        path = self.base_path + '/' + name
401
        ret = self.httpclient.request('HEAD', path)
402
        if ret.status_code == 404:
403
            return None
404
        if ret.status_code < 200 or ret.status_code > 300:
405
            raise SwiftException('HEAD request failed with error code %s'
406
                                 % ret.status_code)
407
        resp_headers = {}
408
        for header, value in ret.items():
409
            resp_headers[header.lower()] = value
410
        return resp_headers
411

    
412
    def put_object(self, name, content):
413
        """Put an object
414

415
        :param name: The object name
416
        :param content: A file object
417
        :raise: `SwiftException` if unable to create
418
        """
419
        content.seek(0)
420
        data = content.read()
421
        path = self.base_path + '/' + name
422
        headers = {'Content-Length': str(len(data))}
423

    
424
        def _send():
425
            ret = self.httpclient.request('PUT', path,
426
                                          body=data,
427
                                          headers=headers)
428
            return ret
429

    
430
        try:
431
            # Sometime got Broken Pipe - Dirty workaround
432
            ret = _send()
433
        except Exception:
434
            # Second attempt work
435
            ret = _send()
436

    
437
        if ret.status_code < 200 or ret.status_code > 300:
438
            raise SwiftException('PUT request failed with error code %s'
439
                                 % ret.status_code)
440

    
441
    def get_object(self, name, range=None):
442
        """Retrieve an object
443

444
        :param name: The object name
445
        :param range: A string range like "0-10" to
446
                      retrieve specified bytes in object content
447
        :return: A file like instance
448
                 or bytestring if range is specified
449
        """
450
        headers = {}
451
        if range:
452
            headers['Range'] = 'bytes=%s' % range
453
        path = self.base_path + '/' + name
454
        ret = self.httpclient.request('GET', path, headers=headers)
455
        if ret.status_code == 404:
456
            return None
457
        if ret.status_code < 200 or ret.status_code > 300:
458
            raise SwiftException('GET request failed with error code %s'
459
                                 % ret.status_code)
460
        content = ret.read()
461

    
462
        if range:
463
            return content
464
        return BytesIO(content)
465

    
466
    def del_object(self, name):
467
        """Delete an object
468

469
        :param name: The object name
470
        :raise: `SwiftException` if unable to delete
471
        """
472
        path = self.base_path + '/' + name
473
        ret = self.httpclient.request('DELETE', path)
474
        if ret.status_code < 200 or ret.status_code > 300:
475
            raise SwiftException('DELETE request failed with error code %s'
476
                                 % ret.status_code)
477

    
478
    def del_root(self):
479
        """Delete the root container by removing container content
480

481
        :raise: `SwiftException` if unable to delete
482
        """
483
        for obj in self.get_container_objects():
484
            self.del_object(obj['name'])
485
        ret = self.httpclient.request('DELETE', self.base_path)
486
        if ret.status_code < 200 or ret.status_code > 300:
487
            raise SwiftException('DELETE request failed with error code %s'
488
                                 % ret.status_code)
489

    
490

    
491
class SwiftPackReader(object):
492
    """A SwiftPackReader that mimic read and sync method
493

494
    The reader allows to read a specified amount of bytes from
495
    a given offset of a Swift object. A read offset is kept internaly.
496
    The reader will read from Swift a specified amount of data to complete
497
    its internal buffer. chunk_length specifiy the amount of data
498
    to read from Swift.
499
    """
500

    
501
    def __init__(self, scon, filename, pack_length):
502
        """Initialize a SwiftPackReader
503

504
        :param scon: a `SwiftConnector` instance
505
        :param filename: the pack filename
506
        :param pack_length: The size of the pack object
507
        """
508
        self.scon = scon
509
        self.filename = filename
510
        self.pack_length = pack_length
511
        self.offset = 0
512
        self.base_offset = 0
513
        self.buff = b''
514
        self.buff_length = self.scon.chunk_length
515

    
516
    def _read(self, more=False):
517
        if more:
518
            self.buff_length = self.buff_length * 2
519
        l = self.base_offset
520
        r = min(self.base_offset + self.buff_length, self.pack_length)
521
        ret = self.scon.get_object(self.filename, range="%s-%s" % (l, r))
522
        self.buff = ret
523

    
524
    def read(self, length):
525
        """Read a specified amount of Bytes form the pack object
526

527
        :param length: amount of bytes to read
528
        :return: bytestring
529
        """
530
        end = self.offset+length
531
        if self.base_offset + end > self.pack_length:
532
            data = self.buff[self.offset:]
533
            self.offset = end
534
            return data
535
        if end > len(self.buff):
536
            # Need to read more from swift
537
            self._read(more=True)
538
            return self.read(length)
539
        data = self.buff[self.offset:end]
540
        self.offset = end
541
        return data
542

    
543
    def seek(self, offset):
544
        """Seek to a specified offset
545

546
        :param offset: the offset to seek to
547
        """
548
        self.base_offset = offset
549
        self._read()
550
        self.offset = 0
551

    
552
    def read_checksum(self):
553
        """Read the checksum from the pack
554

555
        :return: the checksum bytestring
556
        """
557
        return self.scon.get_object(self.filename, range="-20")
558

    
559

    
560
class SwiftPackData(PackData):
561
    """The data contained in a packfile.
562

563
    We use the SwiftPackReader to read bytes from packs stored in Swift
564
    using the Range header feature of Swift.
565
    """
566

    
567
    def __init__(self, scon, filename):
568
        """ Initialize a SwiftPackReader
569

570
        :param scon: a `SwiftConnector` instance
571
        :param filename: the pack filename
572
        """
573
        self.scon = scon
574
        self._filename = filename
575
        self._header_size = 12
576
        headers = self.scon.get_object_stat(self._filename)
577
        self.pack_length = int(headers['content-length'])
578
        pack_reader = SwiftPackReader(self.scon, self._filename,
579
                                      self.pack_length)
580
        (version, self._num_objects) = read_pack_header(pack_reader.read)
581
        self._offset_cache = LRUSizeCache(1024*1024*self.scon.cache_length,
582
                                          compute_size=_compute_object_size)
583
        self.pack = None
584

    
585
    def get_object_at(self, offset):
586
        if offset in self._offset_cache:
587
            return self._offset_cache[offset]
588
        assert offset >= self._header_size
589
        pack_reader = SwiftPackReader(self.scon, self._filename,
590
                                      self.pack_length)
591
        pack_reader.seek(offset)
592
        unpacked, _ = unpack_object(pack_reader.read)
593
        return (unpacked.pack_type_num, unpacked._obj())
594

    
595
    def get_stored_checksum(self):
596
        pack_reader = SwiftPackReader(self.scon, self._filename,
597
                                      self.pack_length)
598
        return pack_reader.read_checksum()
599

    
600
    def close(self):
601
        pass
602

    
603

    
604
class SwiftPack(Pack):
605
    """A Git pack object.
606

607
    Same implementation as pack.Pack except that _idx_load and
608
    _data_load are bounded to Swift version of load_pack_index and
609
    PackData.
610
    """
611

    
612
    def __init__(self, *args, **kwargs):
613
        self.scon = kwargs['scon']
614
        del kwargs['scon']
615
        super(SwiftPack, self).__init__(*args, **kwargs)
616
        self._pack_info_path = self._basename + '.info'
617
        self._pack_info = None
618
        self._pack_info_load = lambda: load_pack_info(self._pack_info_path,
619
                                                      self.scon)
620
        self._idx_load = lambda: swift_load_pack_index(self.scon,
621
                                                       self._idx_path)
622
        self._data_load = lambda: SwiftPackData(self.scon, self._data_path)
623

    
624
    @property
625
    def pack_info(self):
626
        """The pack data object being used."""
627
        if self._pack_info is None:
628
            self._pack_info = self._pack_info_load()
629
        return self._pack_info
630

    
631

    
632
class SwiftObjectStore(PackBasedObjectStore):
633
    """A Swift Object Store
634

635
    Allow to manage a bare Git repository from Openstack Swift.
636
    This object store only supports pack files and not loose objects.
637
    """
638
    def __init__(self, scon):
639
        """Open a Swift object store.
640

641
        :param scon: A `SwiftConnector` instance
642
        """
643
        super(SwiftObjectStore, self).__init__()
644
        self.scon = scon
645
        self.root = self.scon.root
646
        self.pack_dir = posixpath.join(OBJECTDIR, PACKDIR)
647
        self._alternates = None
648

    
649
    @property
650
    def packs(self):
651
        """List with pack objects."""
652
        if not self._pack_cache:
653
            self._update_pack_cache()
654
        return self._pack_cache.values()
655

    
656
    def _update_pack_cache(self):
657
        for pack in self._load_packs():
658
            self._pack_cache[pack._basename] = pack
659

    
660
    def _iter_loose_objects(self):
661
        """Loose objects are not supported by this repository
662
        """
663
        return []
664

    
665
    def iter_shas(self, finder):
666
        """An iterator over pack's ObjectStore.
667

668
        :return: a `ObjectStoreIterator` or `GreenThreadsObjectStoreIterator`
669
                 instance if gevent is enabled
670
        """
671
        shas = iter(finder.next, None)
672
        return PackInfoObjectStoreIterator(
673
            self, shas, finder, self.scon.concurrency)
674

    
675
    def find_missing_objects(self, *args, **kwargs):
676
        kwargs['concurrency'] = self.scon.concurrency
677
        return PackInfoMissingObjectFinder(self, *args, **kwargs)
678

    
679
    def _load_packs(self):
680
        """Load all packs from Swift
681

682
        :return: a list of `SwiftPack` instances
683
        """
684
        objects = self.scon.get_container_objects()
685
        pack_files = [o['name'].replace(".pack", "")
686
                      for o in objects if o['name'].endswith(".pack")]
687
        return [SwiftPack(pack, scon=self.scon) for pack in pack_files]
688

    
689
    def pack_info_get(self, sha):
690
        for pack in self.packs:
691
            if sha in pack:
692
                return pack.pack_info[sha]
693

    
694
    def _collect_ancestors(self, heads, common=set()):
695
        def _find_parents(commit):
696
            for pack in self.packs:
697
                if commit in pack:
698
                    try:
699
                        parents = pack.pack_info[commit][1]
700
                    except KeyError:
701
                        # Seems to have no parents
702
                        return []
703
                    return parents
704

    
705
        bases = set()
706
        commits = set()
707
        queue = []
708
        queue.extend(heads)
709
        while queue:
710
            e = queue.pop(0)
711
            if e in common:
712
                bases.add(e)
713
            elif e not in commits:
714
                commits.add(e)
715
                parents = _find_parents(e)
716
                queue.extend(parents)
717
        return (commits, bases)
718

    
719
    def add_pack(self):
720
        """Add a new pack to this object store.
721

722
        :return: Fileobject to write to and a commit function to
723
            call when the pack is finished.
724
        """
725
        f = BytesIO()
726

    
727
        def commit():
728
            f.seek(0)
729
            pack = PackData(file=f, filename="")
730
            entries = pack.sorted_entries()
731
            if len(entries):
732
                basename = posixpath.join(self.pack_dir,
733
                                          "pack-%s" %
734
                                          iter_sha1(entry[0] for
735
                                                    entry in entries))
736
                index = BytesIO()
737
                write_pack_index_v2(index, entries, pack.get_stored_checksum())
738
                self.scon.put_object(basename + ".pack", f)
739
                f.close()
740
                self.scon.put_object(basename + ".idx", index)
741
                index.close()
742
                final_pack = SwiftPack(basename, scon=self.scon)
743
                final_pack.check_length_and_checksum()
744
                self._add_known_pack(basename, final_pack)
745
                return final_pack
746
            else:
747
                return None
748

    
749
        def abort():
750
            pass
751
        return f, commit, abort
752

    
753
    def add_object(self, obj):
754
        self.add_objects([(obj, None), ])
755

    
756
    def _pack_cache_stale(self):
757
        return False
758

    
759
    def _get_loose_object(self, sha):
760
        return None
761

    
762
    def add_thin_pack(self, read_all, read_some):
763
        """Read a thin pack
764

765
        Read it from a stream and complete it in a temporary file.
766
        Then the pack and the corresponding index file are uploaded to Swift.
767
        """
768
        fd, path = tempfile.mkstemp(prefix='tmp_pack_')
769
        f = os.fdopen(fd, 'w+b')
770
        try:
771
            indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
772
            copier = PackStreamCopier(read_all, read_some, f,
773
                                      delta_iter=indexer)
774
            copier.verify()
775
            return self._complete_thin_pack(f, path, copier, indexer)
776
        finally:
777
            f.close()
778
            os.unlink(path)
779

    
780
    def _complete_thin_pack(self, f, path, copier, indexer):
781
        entries = list(indexer)
782

    
783
        # Update the header with the new number of objects.
784
        f.seek(0)
785
        write_pack_header(f, len(entries) + len(indexer.ext_refs()))
786

    
787
        # Must flush before reading (http://bugs.python.org/issue3207)
788
        f.flush()
789

    
790
        # Rescan the rest of the pack, computing the SHA with the new header.
791
        new_sha = compute_file_sha(f, end_ofs=-20)
792

    
793
        # Must reposition before writing (http://bugs.python.org/issue3207)
794
        f.seek(0, os.SEEK_CUR)
795

    
796
        # Complete the pack.
797
        for ext_sha in indexer.ext_refs():
798
            assert len(ext_sha) == 20
799
            type_num, data = self.get_raw(ext_sha)
800
            offset = f.tell()
801
            crc32 = write_pack_object(f, type_num, data, sha=new_sha)
802
            entries.append((ext_sha, offset, crc32))
803
        pack_sha = new_sha.digest()
804
        f.write(pack_sha)
805
        f.flush()
806

    
807
        # Move the pack in.
808
        entries.sort()
809
        pack_base_name = posixpath.join(
810
            self.pack_dir,
811
            'pack-' + iter_sha1(e[0] for e in entries).decode(sys.getfilesystemencoding()))
812
        self.scon.put_object(pack_base_name + '.pack', f)
813

    
814
        # Write the index.
815
        filename = pack_base_name + '.idx'
816
        index_file = BytesIO()
817
        write_pack_index_v2(index_file, entries, pack_sha)
818
        self.scon.put_object(filename, index_file)
819

    
820
        # Write pack info.
821
        f.seek(0)
822
        pack_data = PackData(filename="", file=f)
823
        index_file.seek(0)
824
        pack_index = load_pack_index_file('', index_file)
825
        serialized_pack_info = pack_info_create(pack_data, pack_index)
826
        f.close()
827
        index_file.close()
828
        pack_info_file = BytesIO(serialized_pack_info)
829
        filename = pack_base_name + '.info'
830
        self.scon.put_object(filename, pack_info_file)
831
        pack_info_file.close()
832

    
833
        # Add the pack to the store and return it.
834
        final_pack = SwiftPack(pack_base_name, scon=self.scon)
835
        final_pack.check_length_and_checksum()
836
        self._add_known_pack(pack_base_name, final_pack)
837
        return final_pack
838

    
839

    
840
class SwiftInfoRefsContainer(InfoRefsContainer):
841
    """Manage references in info/refs object.
842
    """
843

    
844
    def __init__(self, scon, store):
845
        self.scon = scon
846
        self.filename = 'info/refs'
847
        self.store = store
848
        f = self.scon.get_object(self.filename)
849
        if not f:
850
            f = BytesIO(b'')
851
        super(SwiftInfoRefsContainer, self).__init__(f)
852

    
853
    def _load_check_ref(self, name, old_ref):
854
        self._check_refname(name)
855
        f = self.scon.get_object(self.filename)
856
        if not f:
857
            return {}
858
        refs = read_info_refs(f)
859
        if old_ref is not None:
860
            if refs[name] != old_ref:
861
                return False
862
        return refs
863

    
864
    def _write_refs(self, refs):
865
        f = BytesIO()
866
        f.writelines(write_info_refs(refs, self.store))
867
        self.scon.put_object(self.filename, f)
868

    
869
    def set_if_equals(self, name, old_ref, new_ref):
870
        """Set a refname to new_ref only if it currently equals old_ref.
871
        """
872
        if name == 'HEAD':
873
            return True
874
        refs = self._load_check_ref(name, old_ref)
875
        if not isinstance(refs, dict):
876
            return False
877
        refs[name] = new_ref
878
        self._write_refs(refs)
879
        self._refs[name] = new_ref
880
        return True
881

    
882
    def remove_if_equals(self, name, old_ref):
883
        """Remove a refname only if it currently equals old_ref.
884
        """
885
        if name == 'HEAD':
886
            return True
887
        refs = self._load_check_ref(name, old_ref)
888
        if not isinstance(refs, dict):
889
            return False
890
        del refs[name]
891
        self._write_refs(refs)
892
        del self._refs[name]
893
        return True
894

    
895
    def allkeys(self):
896
        try:
897
            self._refs['HEAD'] = self._refs['refs/heads/master']
898
        except KeyError:
899
            pass
900
        return self._refs.keys()
901

    
902

    
903
class SwiftRepo(BaseRepo):
904

    
905
    def __init__(self, root, conf):
906
        """Init a Git bare Repository on top of a Swift container.
907

908
        References are managed in info/refs objects by
909
        `SwiftInfoRefsContainer`. The root attribute is the Swift
910
        container that contain the Git bare repository.
911

912
        :param root: The container which contains the bare repo
913
        :param conf: A ConfigParser object
914
        """
915
        self.root = root.lstrip('/')
916
        self.conf = conf
917
        self.scon = SwiftConnector(self.root, self.conf)
918
        objects = self.scon.get_container_objects()
919
        if not objects:
920
            raise Exception('There is not any GIT repo here : %s' % self.root)
921
        objects = [o['name'].split('/')[0] for o in objects]
922
        if OBJECTDIR not in objects:
923
            raise Exception('This repository (%s) is not bare.' % self.root)
924
        self.bare = True
925
        self._controldir = self.root
926
        object_store = SwiftObjectStore(self.scon)
927
        refs = SwiftInfoRefsContainer(self.scon, object_store)
928
        BaseRepo.__init__(self, object_store, refs)
929

    
930
    def _determine_file_mode(self):
931
        """Probe the file-system to determine whether permissions can be trusted.
932

933
        :return: True if permissions can be trusted, False otherwise.
934
        """
935
        return False
936

    
937
    def _put_named_file(self, filename, contents):
938
        """Put an object in a Swift container
939

940
        :param filename: the path to the object to put on Swift
941
        :param contents: the content as bytestring
942
        """
943
        f = BytesIO()
944
        f.write(contents)
945
        self.scon.put_object(filename, f)
946
        f.close()
947

    
948
    @classmethod
949
    def init_bare(cls, scon, conf):
950
        """Create a new bare repository.
951

952
        :param scon: a `SwiftConnector` instance
953
        :param conf: a ConfigParser object
954
        :return: a `SwiftRepo` instance
955
        """
956
        scon.create_root()
957
        for obj in [posixpath.join(OBJECTDIR, PACKDIR),
958
                    posixpath.join(INFODIR, 'refs')]:
959
            scon.put_object(obj, BytesIO(b''))
960
        ret = cls(scon.root, conf)
961
        ret._init_files(True)
962
        return ret
963

    
964

    
965
class SwiftSystemBackend(Backend):
966

    
967
    def __init__(self, logger, conf):
968
        self.conf = conf
969
        self.logger = logger
970

    
971
    def open_repository(self, path):
972
        self.logger.info('opening repository at %s', path)
973
        return SwiftRepo(path, self.conf)
974

    
975

    
976
def cmd_daemon(args):
977
    """Entry point for starting a TCP git server."""
978
    import optparse
979
    parser = optparse.OptionParser()
980
    parser.add_option("-l", "--listen_address", dest="listen_address",
981
                      default="127.0.0.1",
982
                      help="Binding IP address.")
983
    parser.add_option("-p", "--port", dest="port", type=int,
984
                      default=TCP_GIT_PORT,
985
                      help="Binding TCP port.")
986
    parser.add_option("-c", "--swift_config", dest="swift_config",
987
                      default="",
988
                      help="Path to the configuration file for Swift backend.")
989
    options, args = parser.parse_args(args)
990

    
991
    try:
992
        import gevent
993
        import geventhttpclient
994
    except ImportError:
995
        print("gevent and geventhttpclient libraries are mandatory "
996
              " for use the Swift backend.")
997
        sys.exit(1)
998
    import gevent.monkey
999
    gevent.monkey.patch_socket()
1000
    from dulwich.contrib.swift import load_conf
1001
    from dulwich import log_utils
1002
    logger = log_utils.getLogger(__name__)
1003
    conf = load_conf(options.swift_config)
1004
    backend = SwiftSystemBackend(logger, conf)
1005

    
1006
    log_utils.default_logging_config()
1007
    server = TCPGitServer(backend, options.listen_address,
1008
                          port=options.port)
1009
    server.serve_forever()
1010

    
1011

    
1012
def cmd_init(args):
1013
    import optparse
1014
    parser = optparse.OptionParser()
1015
    parser.add_option("-c", "--swift_config", dest="swift_config",
1016
                      default="",
1017
                      help="Path to the configuration file for Swift backend.")
1018
    options, args = parser.parse_args(args)
1019

    
1020
    conf = load_conf(options.swift_config)
1021
    if args == []:
1022
        parser.error("missing repository name")
1023
    repo = args[0]
1024
    scon = SwiftConnector(repo, conf)
1025
    SwiftRepo.init_bare(scon, conf)
1026

    
1027

    
1028
def main(argv=sys.argv):
1029
    commands = {
1030
        "init": cmd_init,
1031
        "daemon": cmd_daemon,
1032
    }
1033

    
1034
    if len(sys.argv) < 2:
1035
        print("Usage: %s <%s> [OPTIONS...]" % (sys.argv[0], "|".join(commands.keys())))
1036
        sys.exit(1)
1037

    
1038
    cmd = sys.argv[1]
1039
    if not cmd in commands:
1040
        print("No such subcommand: %s" % cmd)
1041
        sys.exit(1)
1042
    commands[cmd](sys.argv[2:])
1043

    
1044
if __name__ == '__main__':
1045
    main()