1
2
3
4
5
6 from __future__ import with_statement
7 import base64
8 import copy
9 from hashlib import md5
10 import logging
11 import mimetypes
12 import os
13 import os.path
14 import re
15
16 from .. import client
17 from ..exceptions import ResourceNotFound, DesignerError, \
18 BulkSaveError
19 from .macros import package_shows, package_views
20 from .. import utils
21
22 if os.name == 'nt':
24 return name.replace("\\", "/")
25
27 return name.replace("/", "\\")
28 else:
31
34
35 logger = logging.getLogger(__name__)
36
38
39 - def __init__(self, path, create=False, docid=None, is_ddoc=True):
40 self.docdir = path
41 self.ignores = []
42 self.is_ddoc = is_ddoc
43
44 ignorefile = os.path.join(path, '.couchappignore')
45 if os.path.exists(ignorefile):
46
47
48 self.ignores = utils.json.load(open(ignorefile, 'r'))
49 if not docid:
50 docid = self.get_id()
51 self.docid = docid
52 self._doc = {'_id': self.docid}
53 if create:
54 self.create()
55
57 """
58 if there is an _id file, docid is extracted from it,
59 else we take the current folder name.
60 """
61 idfile = os.path.join(self.docdir, '_id')
62 if os.path.exists(idfile):
63 docid = utils.read_file(idfile).split("\n")[0].strip()
64 if docid: return docid
65 if self.is_ddoc:
66 return "_design/%s" % os.path.split(self.docdir)[1]
67 else:
68 return os.path.split(self.docdir)[1]
69
71 return "<%s (%s/%s)>" % (self.__class__.__name__, self.docdir, self.docid)
72
75
77 if not os.path.isdir(self.docdir):
78 logger.error("%s directory doesn't exist." % self.docdir)
79
80 rcfile = os.path.join(self.docdir, '.couchapprc')
81 if not os.path.isfile(rcfile):
82 utils.write_json(rcfile, {})
83 else:
84 logger.warning("CouchApp already initialized in %s." % self.docdir)
85
86 - def push(self, dbs, atomic=True, force=False):
87 """Push a doc to a list of database `dburls`. If noatomic is true
88 each attachments will be sent one by one."""
89 for db in dbs:
90 if atomic:
91 doc = self.doc(db, force=force)
92 db.save_doc(doc, force_update=True)
93 else:
94 doc = self.doc(db, with_attachments=False, force=force)
95 db.save_doc(doc, force_update=True)
96
97 attachments = doc.get('_attachments') or {}
98
99 for name, filepath in self.attachments():
100 if name not in attachments:
101 logger.debug("attach %s " % name)
102 db.put_attachment(doc, open(filepath, "r"),
103 name=name)
104
105 logger.debug("%s/%s had been pushed from %s" % (db.uri,
106 self.docid, self.docdir))
107
108
110 att = {}
111 with open(filepath, "rb") as f:
112 re_sp = re.compile('\s')
113 att = {
114 "data": re_sp.sub('',base64.b64encode(f.read())),
115 "content_type": ';'.join(filter(None,
116 mimetypes.guess_type(name)))
117 }
118
119 return att
120
121 - def doc(self, db=None, with_attachments=True, force=False):
122 """ Function to reetrieve document object from
123 document directory. If `with_attachments` is True
124 attachments will be included and encoded"""
125
126 manifest = []
127 objects = {}
128 signatures = {}
129 attachments = {}
130
131 self._doc = {'_id': self.docid}
132
133
134 self._doc.update(self.dir_to_fields(self.docdir, manifest=manifest))
135
136 if not 'couchapp' in self._doc:
137 self._doc['couchapp'] = {}
138
139 self.olddoc = {}
140 if db is not None:
141 try:
142 self.olddoc = db.open_doc(self._doc['_id'])
143 attachments = self.olddoc.get('_attachments') or {}
144 self._doc.update({'_rev': self.olddoc['_rev']})
145 except ResourceNotFound:
146 self.olddoc = {}
147
148 if 'couchapp' in self.olddoc:
149 old_signatures = self.olddoc['couchapp'].get('signatures',
150 {})
151 else:
152 old_signatures = {}
153
154 for name, filepath in self.attachments():
155 signatures[name] = utils.sign_file(filepath)
156 if with_attachments and not old_signatures:
157 logger.debug("attach %s " % name)
158 attachments[name] = self.attachment_stub(name, filepath)
159
160 if old_signatures:
161 for name, signature in old_signatures.items():
162 cursign = signatures.get(name)
163 if not cursign:
164 logger.debug("detach %s " % name)
165 del attachments[name]
166 elif cursign != signature:
167 logger.debug("detach %s " % name)
168 del attachments[name]
169 else:
170 continue
171
172 if with_attachments:
173 for name, filepath in self.attachments():
174 if old_signatures.get(name) != signatures.get(name) or force:
175 logger.debug("attach %s " % name)
176 attachments[name] = self.attachment_stub(name, filepath)
177
178 self._doc['_attachments'] = attachments
179
180 self._doc['couchapp'].update({
181 'manifest': manifest,
182 'objects': objects,
183 'signatures': signatures
184 })
185
186
187 if self.docid.startswith('_design/'):
188 for funs in ['shows', 'lists', 'updates', 'filters',
189 'spatial']:
190 if funs in self._doc:
191 package_shows(self._doc, self._doc[funs], self.docdir,
192 objects)
193
194 if 'validate_doc_update' in self._doc:
195 tmp_dict = dict(validate_doc_update=self._doc[
196 "validate_doc_update"])
197 package_shows( self._doc, tmp_dict, self.docdir,
198 objects)
199 self._doc.update(tmp_dict)
200
201 if 'views' in self._doc:
202
203
204
205 views = {}
206 dmanifest = {}
207 for i, fname in enumerate(manifest):
208 if fname.startswith("views/") and fname != "views/":
209 name, ext = os.path.splitext(fname)
210 if name.endswith('/'):
211 name = name[:-1]
212 dmanifest[name] = i
213
214 for vname, value in self._doc['views'].iteritems():
215 if value and isinstance(value, dict):
216 views[vname] = value
217 else:
218 del manifest[dmanifest["views/%s" % vname]]
219 self._doc['views'] = views
220 package_views(self._doc,self._doc["views"], self.docdir,
221 objects)
222
223 if "fulltext" in self._doc:
224 package_views(self._doc,self._doc["fulltext"], self.docdir,
225 objects)
226
227
228 return self._doc
229
231 for i in self.ignores:
232 match = re.match(i, item)
233 if match:
234 logger.debug("ignoring %s" % item)
235 return True
236 return False
237
238 - def dir_to_fields(self, current_dir='', depth=0,
239 manifest=[]):
240 """ process a directory and get all members """
241
242 fields={}
243 if not current_dir:
244 current_dir = self.docdir
245 for name in os.listdir(current_dir):
246 current_path = os.path.join(current_dir, name)
247 rel_path = _replace_backslash(utils.relpath(current_path, self.docdir))
248 if name.startswith("."):
249 continue
250 elif self.check_ignore(name):
251 continue
252 elif depth == 0 and name.startswith('_'):
253
254 continue
255 elif name == '_attachments':
256 continue
257 elif depth == 0 and (name == 'couchapp' or name == 'couchapp.json'):
258
259 if name == "couchapp":
260 manifest.append('%s/' % rel_path)
261 content = self.dir_to_fields(current_path,
262 depth=depth+1, manifest=manifest)
263 else:
264 manifest.append(rel_path)
265 content = utils.read_json(current_path)
266 if not isinstance(content, dict):
267 content = { "meta": content }
268 if 'signatures' in content:
269 del content['signatures']
270
271 if 'manifest' in content:
272 del content['manifest']
273
274 if 'objects' in content:
275 del content['objects']
276
277 if 'length' in content:
278 del content['length']
279
280 if 'couchapp' in fields:
281 fields['couchapp'].update(content)
282 else:
283 fields['couchapp'] = content
284 elif os.path.isdir(current_path):
285 manifest.append('%s/' % rel_path)
286 fields[name] = self.dir_to_fields(current_path,
287 depth=depth+1, manifest=manifest)
288 else:
289 logger.debug("push %s" % rel_path)
290
291 content = ''
292 if name.endswith('.json'):
293 try:
294 content = utils.read_json(current_path)
295 except ValueError:
296 logger.error("Json invalid in %s" % current_path)
297 else:
298 try:
299 content = utils.read_file(current_path).strip()
300 except UnicodeDecodeError:
301 logger.warning("%s isn't encoded in utf8" % current_path)
302 content = utils.read_file(current_path, utf8=False)
303 try:
304 content.encode('utf-8')
305 except UnicodeError:
306 logger.warning(
307 "plan B didn't work, %s is a binary" % current_path)
308 logger.warning("use plan C: encode to base64")
309 content = "base64-encoded;%s" % base64.b64encode(
310 content)
311
312
313
314 name, ext = os.path.splitext(name)
315 if name in fields:
316 logger.warning(
317 "%(name)s is already in properties. Can't add (%(fqn)s)" % {
318 "name": name, "fqn": rel_path })
319 else:
320 manifest.append(rel_path)
321 fields[name] = content
322 return fields
323
325 """ the function processing directory to yeld
326 attachments. """
327 if os.path.isdir(path):
328 for root, dirs, files in os.walk(path):
329 for dirname in dirs:
330 if dirname.startswith('.'):
331 dirs.remove(dirname)
332 elif self.check_ignore(dirname):
333 dirs.remove(dirname)
334 if files:
335 for filename in files:
336 if filename.startswith('.'):
337 continue
338 elif self.check_ignore(filename):
339 continue
340 else:
341 filepath = os.path.join(root, filename)
342 name = utils.relpath(filepath, path)
343 if vendor is not None:
344 name = os.path.join('vendor', vendor, name)
345 name = _replace_backslash(name)
346 yield (name, filepath)
347
349 """ This function yield a tuple (name, filepath) corresponding
350 to each attachment (vendor included) in the couchapp. `name`
351 is the name of attachment in `_attachments` member and `filepath`
352 the path to the attachment on the disk.
353
354 attachments are processed later to allow us to send attachments inline
355 or one by one.
356 """
357
358 attachdir = os.path.join(self.docdir, "_attachments")
359 for attachment in self._process_attachments(attachdir):
360 yield attachment
361 vendordir = os.path.join(self.docdir, 'vendor')
362 if not os.path.isdir(vendordir):
363 logger.debug("%s don't exist" % vendordir)
364 return
365
366 for name in os.listdir(vendordir):
367 current_path = os.path.join(vendordir, name)
368 if os.path.isdir(current_path):
369 attachdir = os.path.join(current_path, '_attachments')
370 if os.path.isdir(attachdir):
371 for attachment in self._process_attachments(attachdir,
372 vendor=name):
373 yield attachment
374
375 - def index(self, dburl, index):
376 if index is not None:
377 return "%s/%s/%s" % (dburl, self.docid, index)
378 elif os.path.isfile(os.path.join(self.docdir, "_attachments",
379 'index.html')):
380 return "%s/%s/index.html" % (dburl, self.docid)
381 return False
382
383 -def document(path, create=False, docid=None, is_ddoc=True):
384 """ simple function to retrive a doc object from filesystem """
385 return FSDoc(path, create=create, docid=docid, is_ddoc=is_ddoc)
386
387 -def push(path, dbs, atomic=True, force=False, docid=None):
388 """ push a document from the fs to one or more dbs. Identic to
389 couchapp push command """
390 if not isinstance(dbs, (list, tuple)):
391 dbs = [dbs]
392
393 doc = document(path, create=False, docid=docid)
394 doc.push(dbs, atomic=atomic, force=force)
395 docspath = os.path.join(path, '_docs')
396 if os.path.exists(docspath):
397 pushdocs(docspath, dbs, atomic=atomic)
398
399 -def pushapps(path, dbs, atomic=True, export=False, couchapprc=False):
400 """ push all couchapps in one folder like couchapp pushapps command
401 line """
402 if not isinstance(dbs, (list, tuple)):
403 dbs = [dbs]
404
405 apps = []
406 for d in os.listdir(path):
407 appdir = os.path.join(path, d)
408 if os.path.isdir(appdir):
409 if couchapprc and not os.path.isfile(os.path.join(appdir,
410 '.couchapprc')):
411 continue
412 doc = document(appdir)
413 if not atomic:
414 doc.push(dbs, atomic=False)
415 else:
416 apps.append(doc)
417 if apps:
418 if export:
419 docs= [doc.doc() for doc in apps]
420 jsonobj = {'docs': docs}
421 return jsonobj
422 else:
423 for db in dbs:
424 docs = []
425 docs = [doc.doc(db) for doc in apps]
426 try:
427 db.save_docs(docs)
428 except BulkSaveError, e:
429 docs1 = []
430 for doc in e.errors:
431 try:
432 doc['_rev'] = db.last_rev(doc['_id'])
433 docs1.append(doc)
434 except ResourceNotFound:
435 pass
436 if docs1:
437 db.save_docs(docs1)
438
439
440 -def pushdocs(path, dbs, atomic=True, export=False):
441 """ push multiple docs in a path """
442 if not isinstance(dbs, (list, tuple)):
443 dbs = [dbs]
444
445 docs = []
446 for d in os.listdir(path):
447 docdir = os.path.join(path, d)
448 if docdir.startswith('.'):
449 continue
450 elif os.path.isfile(docdir):
451 if d.endswith(".json"):
452 doc = utils.read_json(docdir)
453 docid, ext = os.path.splitext(d)
454 doc.setdefault('_id', docid)
455 doc.setdefault('couchapp', {})
456 if not atomic:
457 for db in dbs:
458 db.save_doc(doc, force_update=True)
459 else:
460 docs.append(doc)
461 else:
462 doc = document(docdir, is_ddoc=False)
463 if not atomic:
464 doc.push(dbs, atomic=False)
465 else:
466 docs.append(doc)
467 if docs:
468 if export:
469 docs1 = []
470 for doc in docs:
471 if hasattr(doc, 'doc'):
472 docs1.append(doc.doc())
473 else:
474 docs1.append(doc)
475 jsonobj = {'docs': docs1}
476 return jsonobj
477 else:
478 for db in dbs:
479 docs1 = []
480 for doc in docs:
481 if hasattr(doc, 'doc'):
482 docs1.append(doc.doc(db))
483 else:
484 newdoc = doc.copy()
485 try:
486 rev = db.last_rev(doc['_id'])
487 newdoc.update({'_rev': rev})
488 except ResourceNotFound:
489 pass
490 docs1.append(newdoc)
491 try:
492 db.save_docs(docs1)
493 except BulkSaveError, e:
494
495 docs1 = []
496 for doc in e.errors:
497 try:
498 doc['_rev'] = db.last_rev(doc['_id'])
499 docs1.append(doc)
500 except ResourceNotFound:
501 pass
502 if docs1:
503 db.save_docs(docs1)
504
505 -def clone(db, docid, dest=None, rev=None):
506 """
507 Clone a CouchDB document to the fs.
508
509 """
510 if not dest:
511 dest = docid
512
513 path = os.path.normpath(os.path.join(os.getcwd(), dest))
514 if not os.path.exists(path):
515 os.makedirs(path)
516
517 if not rev:
518 doc = db.open_doc(docid)
519 else:
520 doc = db.open_doc(docid, rev=rev)
521 docid = doc['_id']
522
523
524 metadata = doc.get('couchapp', {})
525
526
527 manifest = metadata.get('manifest', {})
528
529
530 signatures = metadata.get('signatures', {})
531
532
533 objects = metadata.get('objects', {})
534
535
536 if manifest:
537 for filename in manifest:
538 logger.debug("clone property: %s" % filename)
539 filepath = os.path.join(path, filename)
540 if filename.endswith('/'):
541 if not os.path.isdir(filepath):
542 os.makedirs(filepath)
543 elif filename == "couchapp.json":
544 continue
545 else:
546 parts = utils.split_path(filename)
547 fname = parts.pop()
548 v = doc
549 while 1:
550 try:
551 for key in parts:
552 v = v[key]
553 except KeyError:
554 break
555
556 last_key, ext = os.path.splitext(fname)
557
558
559 try:
560 content = v[last_key]
561 except KeyError:
562 break
563
564
565 if isinstance(content, basestring):
566 _ref = md5(utils.to_bytestring(content)).hexdigest()
567 if objects and _ref in objects:
568 content = objects[_ref]
569
570 if content.startswith('base64-encoded;'):
571 content = base64.b64decode(content[15:])
572
573 if fname.endswith('.json'):
574 content = utils.json.dumps(content).encode('utf-8')
575
576 del v[last_key]
577
578
579 filedir = os.path.dirname(filepath)
580 if not os.path.isdir(filedir):
581 os.makedirs(filedir)
582
583 utils.write_content(filepath, content)
584
585
586 temp = doc
587 for key2 in parts:
588 if key2 == key:
589 if not temp[key2]:
590 del temp[key2]
591 break
592 temp = temp[key2]
593
594
595
596
597 for key in doc.iterkeys():
598 if key.startswith('_'):
599 continue
600 elif key in ('couchapp'):
601 app_meta = copy.deepcopy(doc['couchapp'])
602 if 'signatures' in app_meta:
603 del app_meta['signatures']
604 if 'manifest' in app_meta:
605 del app_meta['manifest']
606 if 'objects' in app_meta:
607 del app_meta['objects']
608 if 'length' in app_meta:
609 del app_meta['length']
610 if app_meta:
611 couchapp_file = os.path.join(path, 'couchapp.json')
612 utils.write_json(couchapp_file, app_meta)
613 elif key in ('views'):
614 vs_dir = os.path.join(path, key)
615 if not os.path.isdir(vs_dir):
616 os.makedirs(vs_dir)
617 for vsname, vs_item in doc[key].iteritems():
618 vs_item_dir = os.path.join(vs_dir, vsname)
619 if not os.path.isdir(vs_item_dir):
620 os.makedirs(vs_item_dir)
621 for func_name, func in vs_item.iteritems():
622 filename = os.path.join(vs_item_dir, '%s.js' %
623 func_name)
624 utils.write_content(filename, func)
625 logger.warning("clone view not in manifest: %s" % filename)
626 elif key in ('shows', 'lists', 'filter', 'update'):
627 showpath = os.path.join(path, key)
628 if not os.path.isdir(showpath):
629 os.makedirs(showpath)
630 for func_name, func in doc[key].iteritems():
631 filename = os.path.join(showpath, '%s.js' %
632 func_name)
633 utils.write_content(filename, func)
634 logger.warning(
635 "clone show or list not in manifest: %s" % filename)
636 else:
637 filedir = os.path.join(path, key)
638 if os.path.exists(filedir):
639 continue
640 else:
641 logger.warning("clone property not in manifest: %s" % key)
642 if isinstance(doc[key], (list, tuple,)):
643 utils.write_json(filedir + ".json", doc[key])
644 elif isinstance(doc[key], dict):
645 if not os.path.isdir(filedir):
646 os.makedirs(filedir)
647 for field, value in doc[key].iteritems():
648 fieldpath = os.path.join(filedir, field)
649 if isinstance(value, basestring):
650 if value.startswith('base64-encoded;'):
651 value = base64.b64decode(content[15:])
652 utils.write_content(fieldpath, value)
653 else:
654 utils.write_json(fieldpath + '.json', value)
655 else:
656 value = doc[key]
657 if not isinstance(value, basestring):
658 value = str(value)
659 utils.write_content(filedir, value)
660
661
662 idfile = os.path.join(path, '_id')
663 utils.write_content(idfile, doc['_id'])
664
665 utils.write_json(os.path.join(path, '.couchapprc'), {})
666
667 if '_attachments' in doc:
668 attachdir = os.path.join(path, '_attachments')
669 if not os.path.isdir(attachdir):
670 os.makedirs(attachdir)
671
672 for filename in doc['_attachments'].iterkeys():
673 if filename.startswith('vendor'):
674 attach_parts = utils.split_path(filename)
675 vendor_attachdir = os.path.join(path, attach_parts.pop(0),
676 attach_parts.pop(0), '_attachments')
677 filepath = os.path.join(vendor_attachdir, *attach_parts)
678 else:
679 filepath = os.path.join(attachdir, filename)
680 filepath = _replace_slash(filepath)
681 currentdir = os.path.dirname(filepath)
682 if not os.path.isdir(currentdir):
683 os.makedirs(currentdir)
684
685 if signatures.get(filename) != utils.sign_file(filepath):
686 stream = db.fetch_attachment(docid, filename, stream=True)
687 with open(filepath, 'wb') as f:
688 for chunk in stream:
689 f.write(chunk)
690 logger.debug("clone attachment: %s" % filename)
691
692 logger.debug("%s/%s cloned in %s" % (db.uri, docid, dest))
693
695 """ Clone a design document from it's url like couchapp does.
696 """
697 try:
698 dburl, docid = source.split('_design/')
699 except ValueError:
700 raise DesignerError("%s isn't a valid source" % source)
701
702 db = client.Database(dburl[:-1], create=False)
703 clone(db, docid, dest, rev=rev)
704