checks.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. """
  2. Checking objects in a GRASS GIS Spatial Database
  3. (C) 2020 by the GRASS Development Team
  4. This program is free software under the GNU General Public
  5. License (>=v2). Read the file COPYING that comes with GRASS
  6. for details.
  7. .. sectionauthor:: Vaclav Petras <wenzeslaus gmail com>
  8. """
  9. import os
  10. import datetime
  11. from pathlib import Path
  12. from grass.script import gisenv
  13. def mapset_exists(database, location, mapset):
  14. """Returns True whether mapset path exists."""
  15. location_path = os.path.join(database, location)
  16. mapset_path = os.path.join(location_path, mapset)
  17. if os.path.exists(mapset_path):
  18. return True
  19. return False
  20. def location_exists(database, location):
  21. """Returns True whether location path exists."""
  22. location_path = os.path.join(database, location)
  23. if os.path.exists(location_path):
  24. return True
  25. return False
  26. # TODO: distinguish between valid for getting maps and usable as current
  27. # https://lists.osgeo.org/pipermail/grass-dev/2016-September/082317.html
  28. # interface created according to the current usage
  29. def is_mapset_valid(mapset_path):
  30. """Return True if GRASS Mapset is valid"""
  31. # WIND is created from DEFAULT_WIND by `g.region -d` and functions
  32. # or modules which create a new mapset. Most modules will fail if
  33. # WIND doesn't exist (assuming that neither GRASS_REGION nor
  34. # WIND_OVERRIDE environmental variables are set).
  35. return os.access(os.path.join(mapset_path, "WIND"), os.R_OK)
  36. def is_location_valid(database, location):
  37. """Return True if GRASS Location is valid
  38. :param database: Path to GRASS GIS database directory
  39. :param location: name of a Location
  40. """
  41. # DEFAULT_WIND file should not be required until you do something
  42. # that actually uses them. The check is just a heuristic; a directory
  43. # containing a PERMANENT/DEFAULT_WIND file is probably a GRASS
  44. # location, while a directory lacking it probably isn't.
  45. return os.access(
  46. os.path.join(database, location, "PERMANENT", "DEFAULT_WIND"), os.F_OK
  47. )
  48. def is_mapset_current(grassdb, location, mapset):
  49. genv = gisenv()
  50. if (grassdb == genv['GISDBASE'] and
  51. location == genv['LOCATION_NAME'] and
  52. mapset == genv['MAPSET']):
  53. return True
  54. return False
  55. def is_location_current(grassdb, location):
  56. genv = gisenv()
  57. if (grassdb == genv['GISDBASE'] and
  58. location == genv['LOCATION_NAME']):
  59. return True
  60. return False
  61. def is_current_user_mapset_owner(mapset_path):
  62. """Returns True if mapset owner is the current user"""
  63. # Note that this does account for libgis built with SKIP_MAPSET_OWN_CHK
  64. # which disables the ownerships check, i.e., even if it was build with the
  65. # skip, it still needs the env variable.
  66. if os.environ.get("GRASS_SKIP_MAPSET_OWNER_CHECK", None):
  67. # Mapset just needs to be accessible for writing.
  68. return os.access(mapset_path, os.W_OK)
  69. # Mapset needs to be owned by user.
  70. stat_info = os.stat(mapset_path)
  71. mapset_uid = stat_info.st_uid
  72. return mapset_uid == os.getuid()
  73. def is_different_mapset_owner(mapset_path):
  74. """Returns True if mapset owner is different from the current user"""
  75. return not is_current_user_mapset_owner(mapset_path)
  76. def get_mapset_owner(mapset_path):
  77. """Returns mapset owner name or None if owner name unknown"""
  78. try:
  79. path = Path(mapset_path)
  80. return path.owner()
  81. except KeyError:
  82. return None
  83. def is_mapset_locked(mapset_path):
  84. """Check if the mapset is locked"""
  85. lock_name = ".gislock"
  86. lockfile = os.path.join(mapset_path, lock_name)
  87. return os.path.exists(lockfile)
  88. def get_lockfile_if_present(database, location, mapset):
  89. """Return path to lock if present, None otherwise
  90. Returns the path as a string or None if nothing was found, so the
  91. return value can be used to test if the lock is present.
  92. """
  93. lock_name = ".gislock"
  94. lockfile = os.path.join(database, location, mapset, lock_name)
  95. if os.path.isfile(lockfile):
  96. return lockfile
  97. return None
  98. def get_mapset_lock_info(mapset_path):
  99. """Get information about .gislock file.
  100. Assumes lock file exists, use is_mapset_locked to find out.
  101. Returns information as a dictionary with keys
  102. 'owner' (None if unknown), 'lockpath', and 'timestamp'.
  103. """
  104. info = {}
  105. lock_name = ".gislock"
  106. info['lockpath'] = os.path.join(mapset_path, lock_name)
  107. try:
  108. info['owner'] = Path(info['lockpath']).owner()
  109. except KeyError:
  110. info['owner'] = None
  111. info['timestamp'] = (datetime.datetime.fromtimestamp(
  112. os.path.getmtime(info['lockpath']))).replace(microsecond=0)
  113. return info
  114. def can_start_in_mapset(mapset_path, ignore_lock=False):
  115. """Check if a mapset from a gisrc file is usable for new session"""
  116. if not is_mapset_valid(mapset_path):
  117. return False
  118. if not is_current_user_mapset_owner(mapset_path):
  119. return False
  120. if not ignore_lock and is_mapset_locked(mapset_path):
  121. return False
  122. return True
  123. def dir_contains_location(path):
  124. """Return True if directory *path* contains a valid location"""
  125. if not os.path.isdir(path):
  126. return False
  127. for name in os.listdir(path):
  128. if os.path.isdir(os.path.join(path, name)):
  129. if is_location_valid(path, name):
  130. return True
  131. return False
  132. # basically checking location, possibly split into two functions
  133. # (mapset one can call location one)
  134. def get_mapset_invalid_reason(database, location, mapset, none_for_no_reason=False):
  135. """Returns a message describing what is wrong with the Mapset
  136. The goal is to provide the most suitable error message
  137. (rather than to do a quick check).
  138. :param database: Path to GRASS GIS database directory
  139. :param location: name of a Location
  140. :param mapset: name of a Mapset
  141. :returns: translated message
  142. """
  143. # Since we are trying to get the one most likely message, we need all
  144. # those return statements here.
  145. # pylint: disable=too-many-return-statements
  146. location_path = os.path.join(database, location)
  147. mapset_path = os.path.join(location_path, mapset)
  148. # first checking the location validity
  149. # perhaps a special set of checks with different messages mentioning mapset
  150. # will be needed instead of the same set of messages used for location
  151. location_msg = get_location_invalid_reason(
  152. database, location, none_for_no_reason=True
  153. )
  154. if location_msg:
  155. return location_msg
  156. # if location is valid, check mapset
  157. if mapset not in os.listdir(location_path):
  158. # TODO: remove the grass.py specific wording
  159. return _(
  160. "Mapset <{mapset}> doesn't exist in GRASS Location <{location}>"
  161. ).format(mapset=mapset, location=location)
  162. if not os.path.isdir(mapset_path):
  163. return _("<%s> is not a GRASS Mapset because it is not a directory") % mapset
  164. if not os.path.isfile(os.path.join(mapset_path, "WIND")):
  165. return (
  166. _(
  167. "<%s> is not a valid GRASS Mapset"
  168. " because it does not have a WIND file"
  169. )
  170. % mapset
  171. )
  172. # based on the is_mapset_valid() function
  173. if not os.access(os.path.join(mapset_path, "WIND"), os.R_OK):
  174. return (
  175. _(
  176. "<%s> is not a valid GRASS Mapset"
  177. " because its WIND file is not readable"
  178. )
  179. % mapset
  180. )
  181. # no reason for invalidity found (might be valid)
  182. if none_for_no_reason:
  183. return None
  184. return _(
  185. "Mapset <{mapset}> or Location <{location}> is invalid for an unknown reason"
  186. ).format(mapset=mapset, location=location)
  187. def get_location_invalid_reason(database, location, none_for_no_reason=False):
  188. """Returns a message describing what is wrong with the Location
  189. The goal is to provide the most suitable error message
  190. (rather than to do a quick check).
  191. By default, when no reason is found, a message about unknown reason is
  192. returned. This applies also to the case when this function is called on
  193. a valid location (e.g. as a part of larger investigation).
  194. ``none_for_no_reason=True`` allows the function to be used as part of other
  195. diagnostic. When this function fails to find reason for invalidity, other
  196. the caller can continue the investigation in their context.
  197. :param database: Path to GRASS GIS database directory
  198. :param location: name of a Location
  199. :param none_for_no_reason: When True, return None when reason is unknown
  200. :returns: translated message or None
  201. """
  202. location_path = os.path.join(database, location)
  203. permanent_path = os.path.join(location_path, "PERMANENT")
  204. # directory
  205. if not os.path.exists(location_path):
  206. return _("Location <%s> doesn't exist") % location_path
  207. # permament mapset
  208. if "PERMANENT" not in os.listdir(location_path):
  209. return (
  210. _(
  211. "<%s> is not a valid GRASS Location"
  212. " because PERMANENT Mapset is missing"
  213. )
  214. % location_path
  215. )
  216. if not os.path.isdir(permanent_path):
  217. return (
  218. _(
  219. "<%s> is not a valid GRASS Location"
  220. " because PERMANENT is not a directory"
  221. )
  222. % location_path
  223. )
  224. # partially based on the is_location_valid() function
  225. if not os.path.isfile(os.path.join(permanent_path, "DEFAULT_WIND")):
  226. return (
  227. _(
  228. "<%s> is not a valid GRASS Location"
  229. " because PERMANENT Mapset does not have a DEFAULT_WIND file"
  230. " (default computational region)"
  231. )
  232. % location_path
  233. )
  234. # no reason for invalidity found (might be valid)
  235. if none_for_no_reason:
  236. return None
  237. return _("Location <{location_path}> is invalid for an unknown reason").format(
  238. location_path=location_path
  239. )
  240. def get_location_invalid_suggestion(database, location):
  241. """Return suggestion what to do when specified location is not valid
  242. It gives suggestion when:
  243. * A mapset was specified instead of a location.
  244. * A GRASS database was specified instead of a location.
  245. """
  246. location_path = os.path.join(database, location)
  247. # a common error is to use mapset instead of location,
  248. # if that's the case, include that info into the message
  249. if is_mapset_valid(location_path):
  250. return _(
  251. "<{location}> looks like a mapset, not a location."
  252. " Did you mean just <{one_dir_up}>?"
  253. ).format(location=location, one_dir_up=database)
  254. # confusion about what is database and what is location
  255. if dir_contains_location(location_path):
  256. return _(
  257. "It looks like <{location}> contains locations."
  258. " Did you mean to specify one of them?"
  259. ).format(location=location)
  260. return None