1 #!/usr/bin/env python3 2 3 """SourceDR project configurations and databases. 4 5 `Project` class holds configuration files, review databases, pattern databases, 6 and `codesearch` index files. 7 """ 8 9 import collections 10 import json 11 import os 12 import shutil 13 14 from sourcedr.codesearch import CodeSearch, PathFilter 15 from sourcedr.pattern_db import PatternDB 16 from sourcedr.review_db import ReviewDB 17 from sourcedr.utils import LockedFile 18 19 20 class Config(object): 21 """SourceDR project configuration file.""" 22 23 DEFAULT_NAME = 'sourcedr.json' 24 25 _PATH_TRAVERSAL_ATTRS = ( 26 'file_ext_blacklist', 'file_name_blacklist', 27 'path_component_blacklist') 28 29 30 @classmethod 31 def get_default_path(cls, project_dir): 32 """Get the default path of the configuration file under a project 33 directory.""" 34 return os.path.join(project_dir, cls.DEFAULT_NAME) 35 36 37 def __init__(self, path): 38 self.path = path 39 40 self.source_dir = None 41 self.file_ext_blacklist = set() 42 self.file_name_blacklist = set() 43 self.path_component_blacklist = set() 44 45 46 def load(self): 47 """Load the project configuration from the JSON file.""" 48 with open(self.path, 'r') as config_fp: 49 config_json = json.load(config_fp) 50 for key, value in config_json.items(): 51 if key == 'source_dir': 52 self.source_dir = value 53 elif key in self._PATH_TRAVERSAL_ATTRS: 54 setattr(self, key, set(value)) 55 else: 56 raise ValueError('unknown config name: ' + key) 57 58 59 def save(self): 60 """Save the project configuration to the JSON file.""" 61 with LockedFile(self.path, 'x') as config_fp: 62 config = collections.OrderedDict() 63 config['source_dir'] = self.source_dir 64 for key in self._PATH_TRAVERSAL_ATTRS: 65 config[key] = sorted(getattr(self, key)) 66 json.dump(config, config_fp, indent=2) 67 68 69 class Project(object): 70 """SourceDR project configuration files and databases.""" 71 72 def __init__(self, project_dir): 73 """Load a project from a given project directory.""" 74 75 project_dir = os.path.abspath(project_dir) 76 self.project_dir = project_dir 77 78 if not os.path.isdir(project_dir): 79 raise ValueError('project directory not found: ' + project_dir) 80 81 # Load configuration files 82 config_path = Config.get_default_path(project_dir) 83 self.config = Config(config_path) 84 self.config.load() 85 86 # Recalculate source directory 87 self.source_dir = os.path.abspath( 88 os.path.join(project_dir, self.config.source_dir)) 89 90 # csearchindex file 91 path_filter = PathFilter(self.config.file_ext_blacklist, 92 self.config.file_name_blacklist, 93 self.config.path_component_blacklist) 94 csearch_index_path = CodeSearch.get_default_path(project_dir) 95 self.codesearch = CodeSearch(self.source_dir, csearch_index_path, 96 path_filter) 97 self.codesearch.add_default_filters() 98 99 # Review database file 100 review_db_path = ReviewDB.get_default_path(project_dir) 101 self.review_db = ReviewDB(review_db_path, self.codesearch) 102 103 # Pattern database file 104 pattern_db_path = PatternDB.get_default_path(project_dir) 105 self.pattern_db = PatternDB(pattern_db_path) 106 107 # Sanity checks 108 self._check_source_dir() 109 self._check_lock_files() 110 111 112 def update_csearch_index(self, remove_existing_index): 113 """Create or update codesearch index.""" 114 self.codesearch.build_index(remove_existing_index) 115 116 117 def update_review_db(self): 118 """Update the entries in the review database.""" 119 patterns, is_regexs = self.pattern_db.load() 120 self.review_db.find(patterns, is_regexs) 121 122 123 def _check_source_dir(self): 124 """Check the availability of the source directory.""" 125 if not os.path.isdir(self.source_dir): 126 raise ValueError('source directory not found: ' + self.source_dir) 127 128 129 def _check_lock_files(self): 130 """Check whether there are some lock files.""" 131 for path in (self.config.path, self.review_db.path, 132 self.pattern_db.path): 133 if LockedFile.is_locked(path): 134 raise ValueError('file locked: ' + path) 135 136 137 @classmethod 138 def create_project_dir(cls, project_dir, source_dir): 139 """Create a directory for a new project and setup default 140 configurations.""" 141 142 if not os.path.isdir(source_dir): 143 raise ValueError('source directory not found: ' + source_dir) 144 145 os.makedirs(project_dir, exist_ok=True) 146 147 # Compute the relative path between project_dir and source_dir 148 project_dir = os.path.abspath(project_dir) 149 source_dir = os.path.relpath(os.path.abspath(source_dir), project_dir) 150 151 # Copy default files 152 defaults_dir = os.path.join(os.path.dirname(__file__), 'defaults') 153 for name in (Config.DEFAULT_NAME, PatternDB.DEFAULT_NAME): 154 shutil.copyfile(os.path.join(defaults_dir, name), 155 os.path.join(project_dir, name)) 156 157 # Update the source directory in the configuration file 158 config_path = Config.get_default_path(project_dir) 159 config = Config(config_path) 160 config.load() 161 config.source_dir = source_dir 162 config.save() 163 164 return Project(project_dir) 165 166 167 @classmethod 168 def get_or_create_project_dir(cls, project_dir, source_dir): 169 config_file_path = Config.get_default_path(project_dir) 170 if os.path.exists(config_file_path): 171 return Project(project_dir) 172 else: 173 return cls.create_project_dir(project_dir, source_dir) 174