mirror of
				https://github.com/wting/autojump
				synced 2025-06-13 12:54:07 +00:00 
			
		
		
		
	Move database logic into database object
This commit is contained in:
		
							parent
							
								
									da3d660b50
								
							
						
					
					
						commit
						46b8d84c7e
					
				
							
								
								
									
										265
									
								
								bin/autojump
									
									
									
									
									
								
							
							
						
						
									
										265
									
								
								bin/autojump
									
									
									
									
									
								
							| @ -2,155 +2,176 @@ | ||||
| from __future__ import division, print_function | ||||
| 
 | ||||
| import argparse | ||||
| from sys import argv, stderr, version_info, exit, getfilesystemencoding | ||||
| import sys | ||||
| from tempfile import NamedTemporaryFile | ||||
| from operator import itemgetter | ||||
| import os | ||||
| import shutil | ||||
| 
 | ||||
| AUTOJUMP_VERSION = "release-v20" | ||||
| AUTOJUMP_VERSION = 'release-v20' | ||||
| MAX_KEYWEIGHT = 1000 | ||||
| MAX_STORED_PATHS = 600 | ||||
| COMPLETION_SEPARATOR = '__' | ||||
| 
 | ||||
| if "AUTOJUMP_DATA_DIR" in os.environ: | ||||
|     CONFIG_DIR = os.environ.get("AUTOJUMP_DATA_DIR") | ||||
| if 'AUTOJUMP_DATA_DIR' in os.environ: | ||||
|     CONFIG_DIR = os.environ.get('AUTOJUMP_DATA_DIR') | ||||
| else: | ||||
|     xdg_data_dir = os.environ.get('XDG_DATA_HOME') or os.path.join(os.environ['HOME'], '.local', 'share') | ||||
|     CONFIG_DIR=os.path.join(xdg_data_dir, 'autojump') | ||||
|     CONFIG_DIR = os.path.join(xdg_data_dir, 'autojump') | ||||
| 
 | ||||
| def uniqadd(collection, key): | ||||
|     """Adds a key to a list only if it is not already present""" | ||||
|     if key not in collection: | ||||
|         collection.append(key) | ||||
| if CONFIG_DIR == os.path.expanduser('~'): | ||||
|     DB_FILE = CONFIG_DIR + '/.autojump.txt' | ||||
| else: | ||||
|     DB_FILE = CONFIG_DIR + '/autojump.txt' | ||||
| 
 | ||||
| def dicadd(dic, key, increment=1): | ||||
|     """Increment a value in a dic, set it to 0 | ||||
|     if is is not already present""" | ||||
|     dic[key] = dic.get(key, 0.)+increment | ||||
| class Database: | ||||
|     """ Object for interfacing with autojump database. """ | ||||
| 
 | ||||
|     def __init__(self, filename): | ||||
|         self.filename = filename | ||||
|         self.data = {} | ||||
|         self.load() | ||||
| 
 | ||||
|     def add(self, key, increment = 1): | ||||
|         """ Increment existing paths or initialize new ones to 0. """ | ||||
|         self.data[key] = self.data.get(key, 0.) + increment | ||||
| 
 | ||||
|     def decay(self): | ||||
|         """ Decay database entries. """ | ||||
|         for k in self.data.keys(): | ||||
|             self.data[k] *= 0.9 | ||||
| 
 | ||||
|     def load(self, error_recovery = False): | ||||
|         """ Try to open the database file, recovering from backup if needed. """ | ||||
|         try: | ||||
|             with open(self.filename, 'r') as aj_file: | ||||
|                 for line in aj_file.readlines(): | ||||
|                     weight, path = line[:-1].split("\t", 1) | ||||
|                     path = decode(path, 'utf-8') | ||||
|                     self.data[path] = float(weight) | ||||
|         except (IOError, EOFError): | ||||
|             if not error_recovery and os.path.exists(self.filename + ".bak"): | ||||
|                 print('Problem with autojump database,\ | ||||
|                         trying to recover from backup...', file=sys.stderr) | ||||
|                 shutil.copy(self.filename + '.bak', self.filename) | ||||
|                 return self.load(True) | ||||
|             else: | ||||
|                 # TODO: migration code, will be removed in v22+ | ||||
|                 # autojump_py last used in v17 | ||||
|                 self.filename = get_db_file('autojump_py') | ||||
|                 if os.path.exists(self.filename): | ||||
|                     try: # fix to get optimised pickle in python < 3 | ||||
|                         import cPickle as pickle | ||||
|                     except ImportError: | ||||
|                         import pickle | ||||
|                     try: | ||||
|                         with open(self.filename, 'rb') as aj_file: | ||||
|                             # encoding is only specified for Python 2 compatibility | ||||
|                             if sys.version_info[0] > 2: | ||||
|                                 self.data = pickle.load(aj_file, encoding="utf-8") | ||||
|                             else: | ||||
|                                 self.data = pickle.load(aj_file) | ||||
|                         unicode_dict = {} | ||||
|                         for k, v in self.data.items(): | ||||
|                             print(k) | ||||
|                             unicode_dict[decode(k, errors="replace")] = v | ||||
|                         return unicode_dict | ||||
|                     except (IOError, EOFError, pickle.UnpicklingError): | ||||
|                         pass | ||||
|                 return {} # if everything fails, return an empty dictionary | ||||
| 
 | ||||
|     def maintenance(self): | ||||
|         """ Trims and decays database entries when exceeding settings. """ | ||||
|         if sum(self.data.values()) > MAX_KEYWEIGHT: | ||||
|             self.decay() | ||||
|         if len(self.data) > MAX_STORED_PATHS: | ||||
|             self.trim() | ||||
|         self.save() | ||||
| 
 | ||||
|     def save(self): | ||||
|         """ Save database atomically and preserve backup. """ | ||||
|         # check file existence and permissions | ||||
|         if ((not os.path.exists(self.filename)) or | ||||
|                 os.name == 'nt' or | ||||
|                 os.getuid() == os.stat(self.filename)[4]): | ||||
|             temp = NamedTemporaryFile(dir = CONFIG_DIR, delete = False) | ||||
|             for path, weight in sorted(self.data.items(), | ||||
|                     key=itemgetter(1), | ||||
|                     reverse=True): | ||||
|                 temp.write((unico("%s\t%s\n")%(weight, path)).encode("utf-8")) | ||||
| 
 | ||||
|             # catching disk errors and skipping save when file handle can't be closed. | ||||
|             try: | ||||
|                 # http://thunk.org/tytso/blog/2009/03/15/dont-fear-the-fsync/ | ||||
|                 temp.flush() | ||||
|                 os.fsync(temp) | ||||
|                 temp.close() | ||||
|             except IOError as ex: | ||||
|                 print("Error saving autojump database (disk full?)" % | ||||
|                         ex, file=sys.stderr) | ||||
|                 return | ||||
| 
 | ||||
|             shutil.move(temp.name, self.filename) | ||||
|             try: # backup file | ||||
|                 import time | ||||
|                 if (not os.path.exists(self.filename+".bak") or | ||||
|                         time.time()-os.path.getmtime(self.filename+".bak") > 86400): | ||||
|                     shutil.copy(self.filename, self.filename+".bak") | ||||
|             except OSError as ex: | ||||
|                 print("Error while creating backup autojump file. (%s)" % | ||||
|                         ex, file=sys.stderr) | ||||
| 
 | ||||
|                 def trim(self): | ||||
|                     """ If database has exceeded MAX_STORED_PATHS, removes bottom 10%. """ | ||||
|         dirs = list(self.data.items()) | ||||
|         dirs.sort(key=itemgetter(1)) | ||||
|         remove_cnt = .1 * MAX_STORED_PATHS | ||||
|         for path, _ in dirs[:remove_cnt]: | ||||
|             del self.data[path] | ||||
| 
 | ||||
|     def trim(self): | ||||
|         """ If database has exceeded MAX_STORED_PATHS, removes bottom 10%. """ | ||||
|         dirs = list(self.data.items()) | ||||
|         dirs.sort(key=itemgetter(1)) | ||||
|         remove_cnt = .1 * MAX_STORED_PATHS | ||||
|         for path, _ in dirs[:remove_cnt]: | ||||
|             del self.data[path] | ||||
| 
 | ||||
| 
 | ||||
| def get_db_file(filename = "autojump.txt"): | ||||
|     """ Retrieve full database path. """ | ||||
|     # TODO: Remove when migration code is removed. | ||||
|     if CONFIG_DIR == os.path.expanduser("~"): | ||||
|         return CONFIG_DIR + "/." + filename | ||||
|     else: | ||||
|         return CONFIG_DIR + "/" + filename | ||||
| 
 | ||||
| def output(unicode_text,encoding=None): | ||||
|     """Wrapper for the print function, using the filesystem encoding by default | ||||
|     to minimize encoding mismatch problems in directory names""" | ||||
|     if version_info[0] > 2: | ||||
|     if sys.version_info[0] > 2: | ||||
|         print(unicode_text) | ||||
|     else: | ||||
|         if encoding is None: | ||||
|             encoding = getfilesystemencoding() | ||||
|             encoding = sys.getfilesystemencoding() | ||||
|         print(unicode_text.encode(encoding)) | ||||
| 
 | ||||
| def decode(text,encoding=None,errors="strict"): | ||||
|     """Decoding step for python2.x which does not default to unicode""" | ||||
|     if version_info[0] > 2: | ||||
|     if sys.version_info[0] > 2: | ||||
|         return text | ||||
|     else: | ||||
|         if encoding is None: | ||||
|             encoding = getfilesystemencoding() | ||||
|             encoding = sys.getfilesystemencoding() | ||||
|         return text.decode(encoding,errors) | ||||
| 
 | ||||
| def unico(text): | ||||
|     """if python2, convert to a unicode object""" | ||||
|     if version_info[0] > 2: | ||||
|     if sys.version_info[0] > 2: | ||||
|         return text | ||||
|     else: | ||||
|         return unicode(text) | ||||
| 
 | ||||
| def save(path_dict, dic_file): | ||||
|     """Save the database in an atomic way, and preserve | ||||
|        a backup file.""" | ||||
|     # If the dic_file exists and os supports permissions, check that dic_file belongs to us | ||||
|     # Otherwise, fail quietly | ||||
|     if (not os.path.exists(dic_file)) or os.name == 'nt' or os.getuid() == os.stat(dic_file)[4]: | ||||
|         temp = NamedTemporaryFile(dir=CONFIG_DIR, delete=False) | ||||
|         for path,weight in sorted(path_dict.items(),key=itemgetter(1),reverse=True): | ||||
|             # the db is stored in utf-8 | ||||
|             temp.write((unico("%s\t%s\n")%(weight,path)).encode("utf-8")) | ||||
| 
 | ||||
|         # Catching disk errors and skipping save since file handle can't be closed. | ||||
|         try: | ||||
|             #cf. http://thunk.org/tytso/blog/2009/03/15/dont-fear-the-fsync/ | ||||
|             temp.flush() | ||||
|             os.fsync(temp) | ||||
|             temp.close() | ||||
|         except IOError as ex: | ||||
|             print("Error while saving autojump database (disk full?)" % | ||||
|                     ex, file=stderr) | ||||
|             return | ||||
| 
 | ||||
|         # Use shutil.move instead of os.rename because windows doesn't support | ||||
|         # using rename to overwrite files | ||||
|         shutil.move(temp.name, dic_file) | ||||
|         try: #backup file | ||||
|             import time | ||||
|             if (not os.path.exists(dic_file+".bak") or | ||||
|                     time.time()-os.path.getmtime(dic_file+".bak")>86400): | ||||
|                 shutil.copy(dic_file, dic_file+".bak") | ||||
|         except OSError as ex: | ||||
|             print("Error while creating backup autojump file. (%s)" % | ||||
|                     ex, file=stderr) | ||||
| 
 | ||||
| def open_dic(dic_file, error_recovery=False): | ||||
|     """Try hard to open the database file, recovering | ||||
|        from backup if needed. """ | ||||
|     try: | ||||
|         path_dict = {} | ||||
|         with open(dic_file, 'r') as aj_file: | ||||
|             for l in aj_file.readlines(): | ||||
|                 weight,path = l[:-1].split("\t",1) | ||||
|                 # the db is stored in utf-8 | ||||
|                 path = decode(path,"utf-8") | ||||
|                 path_dict[path] = float(weight) | ||||
|             return path_dict | ||||
|     except (IOError, EOFError): | ||||
|         if not error_recovery and os.path.exists(dic_file+".bak"): | ||||
|             print('Problem with autojump database,\ | ||||
|                     trying to recover from backup...', file=stderr) | ||||
|             shutil.copy(dic_file+".bak", dic_file) | ||||
|             return open_dic(dic_file, True) | ||||
|         else: | ||||
|             # Temporary migration code | ||||
|             old_dic_file = get_dic_file("autojump_py") | ||||
|             if os.path.exists(old_dic_file): | ||||
|                 try: # fix to get optimised pickle in python < 3 | ||||
|                     import cPickle as pickle | ||||
|                 except ImportError: | ||||
|                     import pickle | ||||
|                 try: | ||||
|                     with open(old_dic_file, 'rb') as aj_file: | ||||
|                         if version_info[0] > 2: | ||||
|                             #encoding is only specified for python2.x compatibility | ||||
|                             path_dict = pickle.load(aj_file, encoding="utf-8") | ||||
|                         else: | ||||
|                             path_dict = pickle.load(aj_file) | ||||
|                     unicode_dict = {} #we now use unicode internally | ||||
|                     for k,v in path_dict.items(): | ||||
|                         print(k) | ||||
|                         unicode_dict[decode(k,errors="replace")] = v | ||||
|                     return unicode_dict | ||||
|                 except (IOError, EOFError, pickle.UnpicklingError): | ||||
|                     pass | ||||
|             return {} #if everything fails, return an empty file | ||||
| 
 | ||||
| 
 | ||||
| def forget(path_dict, dic_file): | ||||
|     """Gradually forget about directories. Only call | ||||
|     from the actual jump since it can take time""" | ||||
|     keyweight = sum(path_dict.values()) | ||||
|     if keyweight > MAX_KEYWEIGHT: | ||||
|         for k in path_dict.keys(): | ||||
|             path_dict[k] *= 0.9 * MAX_KEYWEIGHT / keyweight | ||||
|         save(path_dict, dic_file) | ||||
| 
 | ||||
| def clean_dict(sorted_dirs, path_dict): | ||||
|     """Limits the sized of the path_dict to MAX_STORED_PATHS. | ||||
|     Returns True if keys were deleted""" | ||||
|     if len(sorted_dirs) > MAX_STORED_PATHS: | ||||
|         #remove 25 more than needed, to avoid doing it every time | ||||
|         for path, dummy in sorted_dirs[MAX_STORED_PATHS-25:]: | ||||
|             del path_dict[path] | ||||
|         return True | ||||
|     else: return False | ||||
| 
 | ||||
| def match(path, pattern, ignore_case=False, only_end=False): | ||||
|     """Check whether a path matches a particular pattern, and return | ||||
|        the remaning part of the string""" | ||||
| @ -185,17 +206,11 @@ def find_matches(dirs, patterns, result_list, ignore_case, max_matches, current_ | ||||
|         #If a path doesn't exist, don't jump there | ||||
|         #We still keep it in db in case it's from a removable drive | ||||
|         if does_match and os.path.exists(path): | ||||
|             uniqadd(result_list, path) | ||||
|             if path not in result_list: | ||||
|                 result_list.append(path) | ||||
|             if len(result_list) >= max_matches : | ||||
|                 break | ||||
| 
 | ||||
| def get_dic_file(filename="autojump.txt"): | ||||
|     if CONFIG_DIR == os.path.expanduser("~"): | ||||
|         dic_file = CONFIG_DIR+"/." + filename | ||||
|     else: | ||||
|         dic_file = CONFIG_DIR+"/" + filename | ||||
|     return dic_file | ||||
| 
 | ||||
| def shell_utility(): | ||||
|     """Run this when autojump is called as a shell utility""" | ||||
|     parser = argparse.ArgumentParser(description='Automatically jump to directory passed as an argument.', | ||||
| @ -214,8 +229,7 @@ def shell_utility(): | ||||
|             help='show version information and exit') | ||||
| 
 | ||||
|     args = parser.parse_args() | ||||
|     dic_file = get_dic_file() | ||||
|     path_dict = open_dic(dic_file) | ||||
|     db = Database(DB_FILE) | ||||
| 
 | ||||
|     # The home dir can be reached quickly by "cd" and may interfere with other directories | ||||
|     if (args.add): | ||||
| @ -306,5 +320,4 @@ def shell_utility(): | ||||
|         return True | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     success=shell_utility() | ||||
|     if not success: exit(1) | ||||
|     if not shell_utility(): sys.exit(1) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user