|
| 1 | +import json |
| 2 | +from pathlib import Path |
| 3 | + |
| 4 | +import arcgis |
| 5 | + |
| 6 | +UPLOAD_ITEM_TITLE = "moonwalk-restore" |
| 7 | + |
| 8 | + |
| 9 | +def _get_secrets(): |
| 10 | + """A helper method for loading secrets from either a GCF mount point or a local secrets folder. |
| 11 | + json file |
| 12 | +
|
| 13 | + Raises: |
| 14 | + FileNotFoundError: If the secrets file can't be found. |
| 15 | +
|
| 16 | + Returns: |
| 17 | + dict: The secrets .json loaded as a dictionary |
| 18 | + """ |
| 19 | + |
| 20 | + secret_folder = Path("/secrets") |
| 21 | + |
| 22 | + #: Try to get the secrets from the Cloud Function mount point |
| 23 | + if secret_folder.exists(): |
| 24 | + return json.loads(Path("/secrets/app/secrets.json").read_text(encoding="utf-8")) |
| 25 | + |
| 26 | + #: Otherwise, try to load a local copy for local development |
| 27 | + secret_folder = Path(__file__).parent / "secrets" |
| 28 | + if secret_folder.exists(): |
| 29 | + return json.loads((secret_folder / "secrets.json").read_text(encoding="utf-8")) |
| 30 | + |
| 31 | + raise FileNotFoundError("Secrets folder not found; secrets not loaded.") |
| 32 | + |
| 33 | + |
| 34 | +def cleanup_restores(gis): |
| 35 | + print("Cleaning up any old restores...") |
| 36 | + items = gis.content.search(query=f"title:{UPLOAD_ITEM_TITLE}") |
| 37 | + for item in items: |
| 38 | + item.delete(permanent=True) |
| 39 | + |
| 40 | + |
| 41 | +def truncate_and_append(item_id, sub_folder, gis, item): |
| 42 | + fgdb_item = upload_fgdb(item_id, sub_folder, gis) |
| 43 | + |
| 44 | + collection = arcgis.features.FeatureLayerCollection.fromitem(item) |
| 45 | + |
| 46 | + for layer in collection.layers: |
| 47 | + print(f"truncating layer: {layer.properties.name}") |
| 48 | + layer.manager.truncate(asynchronous=True, wait=True) |
| 49 | + print("appending") |
| 50 | + layer.append( |
| 51 | + item_id=fgdb_item.id, |
| 52 | + upload_format="filegdb", |
| 53 | + source_table_name=layer.properties.name, |
| 54 | + return_messages=True, |
| 55 | + rollback=True, |
| 56 | + ) |
| 57 | + |
| 58 | + for table in collection.tables: |
| 59 | + print(f"truncating table: {table.properties.name}") |
| 60 | + table.manager.truncate(asynchronous=True, wait=True) |
| 61 | + print("appending") |
| 62 | + table.append( |
| 63 | + item_id=fgdb_item.id, |
| 64 | + upload_format="filegdb", |
| 65 | + source_table_name=table.properties.name, |
| 66 | + return_messages=True, |
| 67 | + rollback=True, |
| 68 | + ) |
| 69 | + |
| 70 | + fgdb_item.delete(permanent=True) |
| 71 | + |
| 72 | + |
| 73 | +def upload_fgdb(item_id, sub_folder, gis): |
| 74 | + print("uploading fgdb") |
| 75 | + zip_path = Path(f"./temp/sample-bucket/{item_id}/{sub_folder}/data.zip") |
| 76 | + fgdb_item = gis.content.add( |
| 77 | + item_properties={ |
| 78 | + "type": "File Geodatabase", |
| 79 | + "title": UPLOAD_ITEM_TITLE, |
| 80 | + "snippet": "temporary upload from moonwalk", |
| 81 | + }, |
| 82 | + data=str(zip_path), |
| 83 | + ) |
| 84 | + |
| 85 | + return fgdb_item |
| 86 | + |
| 87 | + |
| 88 | +def recreate_item(item_id, sub_folder, gis): |
| 89 | + print("Item not found; creating new item...") |
| 90 | + |
| 91 | + fgdb_item = upload_fgdb(item_id, sub_folder, gis) |
| 92 | + |
| 93 | + original_item_properties = json.loads( |
| 94 | + Path(f"./temp/sample-bucket/{item_id}/{sub_folder}/item.json").read_text(encoding="utf-8") |
| 95 | + ) |
| 96 | + |
| 97 | + print("publishing") |
| 98 | + #: todo: should we worry about restoring layer ids? |
| 99 | + published_item = fgdb_item.publish( |
| 100 | + publish_parameters={"name": original_item_properties.get("name")}, |
| 101 | + ) |
| 102 | + success = published_item.reassign_to( |
| 103 | + target_owner=original_item_properties.get("owner"), |
| 104 | + target_folder=original_item_properties.get("ownerFolder"), |
| 105 | + ) |
| 106 | + if not success: |
| 107 | + raise Exception("Failed to reassign item") |
| 108 | + |
| 109 | + #: sharing |
| 110 | + published_item.sharing.sharing_level = original_item_properties.get("access") |
| 111 | + #: todo: group sharing... |
| 112 | + |
| 113 | + print("updating item") |
| 114 | + supported_property_names = [ |
| 115 | + "description", |
| 116 | + "title", |
| 117 | + "tags", |
| 118 | + "snippet", |
| 119 | + "extent", |
| 120 | + "accessInformation", |
| 121 | + "licenseInfo", |
| 122 | + "culture", |
| 123 | + "access", |
| 124 | + ] |
| 125 | + supported_properties = {k: v for k, v in original_item_properties.items() if k in supported_property_names} |
| 126 | + success = published_item.update( |
| 127 | + item_properties=supported_properties, |
| 128 | + ) |
| 129 | + #: metadata? |
| 130 | + |
| 131 | + print("deleting fgdb") |
| 132 | + #: get a new item reference since the owner has changed |
| 133 | + gis.content.get(fgdb_item.id).delete(permanent=True) |
| 134 | + |
| 135 | + if not success: |
| 136 | + raise Exception("Failed to update item") |
| 137 | + |
| 138 | + print(f"new item created: {published_item.id}") |
| 139 | + |
| 140 | + |
| 141 | +def restore(item_id, sub_folder): |
| 142 | + secrets = _get_secrets() |
| 143 | + gis = arcgis.GIS( |
| 144 | + url=secrets["AGOL_ORG"], |
| 145 | + username=secrets["AGOL_USER"], |
| 146 | + password=secrets["AGOL_PASSWORD"], |
| 147 | + ) |
| 148 | + |
| 149 | + cleanup_restores(gis) |
| 150 | + |
| 151 | + print(f"Restoring {item_id} from {sub_folder}...") |
| 152 | + |
| 153 | + item_exists = True |
| 154 | + try: |
| 155 | + item = arcgis.gis.Item(gis, item_id) |
| 156 | + except Exception: |
| 157 | + item_exists = False |
| 158 | + |
| 159 | + if item_exists: |
| 160 | + if item.type == arcgis.gis.ItemTypeEnum.FEATURE_SERVICE.value: |
| 161 | + truncate_and_append(item_id, sub_folder, gis, item) |
| 162 | + else: |
| 163 | + raise NotImplementedError(f"Unsupported item type: {item.type}") |
| 164 | + #: this breaks web maps and experience builder projects, not sure why |
| 165 | + # print(f"overwriting item: {item_id} from {sub_folder}") |
| 166 | + # success = item.update( |
| 167 | + # item_properties=json.loads( |
| 168 | + # Path(f"./temp/sample-bucket/{item_id}/{sub_folder}/item.json").read_text(encoding="utf-8") |
| 169 | + # ), |
| 170 | + # data=str(Path(f"./temp/sample-bucket/{item_id}/{sub_folder}/data.json")), |
| 171 | + # ) |
| 172 | + # if not success: |
| 173 | + # print("Failed to update item") |
| 174 | + # return |
| 175 | + else: |
| 176 | + recreate_item(item_id, sub_folder, gis) |
| 177 | + |
| 178 | + print("Restore complete!") |
| 179 | + |
| 180 | + |
| 181 | +def local_restore(): |
| 182 | + #: truncate and load |
| 183 | + restore("3ac0f9833f7d4335acebd62fe0695635", "short") |
| 184 | + |
| 185 | + #: recreate feature service |
| 186 | + # restore("33e9c822af3b4d08844d58169410f9fa", "short") |
| 187 | + |
| 188 | + |
| 189 | +if __name__ == "__main__": |
| 190 | + local_restore() |
0 commit comments