[{"data":1,"prerenderedAt":1967},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Ffix-broken-layer-paths-in-project-pyqgis":3},{"id":4,"title":5,"body":6,"description":1956,"extension":1957,"meta":1958,"navigation":280,"path":1963,"seo":1964,"stem":1965,"__hash__":1966},"docs\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Ffix-broken-layer-paths-in-project-pyqgis\u002Findex.md","Fix Broken Layer Paths in a Project with PyQGIS",{"type":7,"value":8,"toc":1943},"minimark",[9,13,31,40,215,220,237,241,248,432,442,445,606,627,631,634,726,1102,1114,1118,1133,1249,1258,1262,1265,1576,1606,1610,1613,1685,1768,1781,1785,1809,1813,1848,1852,1858,1862,1872,1881,1894,1905,1909,1939],[10,11,5],"h1",{"id":12},"fix-broken-layer-paths-in-a-project-with-pyqgis",[14,15,16,17,21,22,25,26,30],"p",{},"A project file does not contain data, only directions to it. Move a share from ",[18,19,20],"code",{},"S:\\"," to ",[18,23,24],{},"\\\\fileserver\\gis",", rename a database host, or copy a project to a colleague's laptop, and every direction points somewhere that no longer exists. QGIS Desktop offers the ",[27,28,29],"em",{},"Handle Unavailable Layers"," dialog, which is fine for one project and three layers. For two hundred projects on a file server after a migration, you want a script.",[14,32,33,34,39],{},"This recipe belongs to ",[35,36,38],"a",{"href":37},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002F","Layer Data Sources & Formats in PyQGIS",". It opens projects without trying to load their data, lists what is broken, rewrites the sources with a mapping table, and saves the result with relative paths so the next move is harmless.",[14,41,42],{},[43,44,49,53,57,64,81,90,100,106,111,115,122,127,131,134,138,141,146,150,153,156,160,164,168,171,174,178,184,189,192,195,202,206,211],"svg",{"viewBox":45,"role":46,"ariaLabel":47,"xmlns":48},"0 0 760 300","img","The repair pipeline: read the project with layer resolution disabled, find invalid layers, decode each source, apply a prefix mapping, set the new data source, and write the project","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[50,51,52],"title",{},"Open without loading, remap, save",[54,55,56],"desc",{},"Five stages left to right. Read with the do-not-resolve-layers flag so nothing tries to open the missing data. Collect the layers that are not valid. Decode each source with the provider registry. Apply an old-prefix to new-prefix mapping table to the path. Set the new source on the layer and write the project, optionally switching to relative paths.",[58,59],"rect",{"x":60,"y":60,"width":61,"height":62,"fill":63},"0","760","300","#f6f3ea",[65,66,67],"defs",{},[68,69,76],"marker",{"id":70,"viewBox":71,"refX":72,"refY":73,"markerWidth":74,"markerHeight":74,"orient":75},"fixPathArrow","0 0 10 10","8","5","7","auto-start-reverse",[77,78],"path",{"d":79,"fill":80},"M0 0 L10 5 L0 10 z","#2f3b35",[82,83,89],"text",{"x":84,"y":85,"style":86,"fill":87,"textAnchor":88},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Repair without ever opening the missing data",[58,91],{"x":92,"y":93,"width":94,"height":95,"rx":96,"fill":97,"stroke":98,"style":99},"16","60","132","96","10","#eef7f4","#0f766e","stroke-width:2",[82,101,105],{"x":102,"y":103,"style":104,"fill":98,"textAnchor":88},"82","88","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","1 read",[82,107,110],{"x":102,"y":108,"style":109,"fill":80,"textAnchor":88},"112","text-anchor:middle;font-size:9.5px;font-family:sans-serif","don't resolve",[82,112,114],{"x":102,"y":113,"style":109,"fill":80,"textAnchor":88},"130","layers",[116,117],"line",{"x1":118,"y1":119,"x2":120,"y2":119,"stroke":80,"style":121},"148","108","162","stroke-width:1.8;marker-end:url(#fixPathArrow)",[58,123],{"x":124,"y":93,"width":94,"height":95,"rx":96,"fill":125,"stroke":126,"style":99},"168","#fdf2e2","#b91c1c",[82,128,130],{"x":129,"y":103,"style":104,"fill":126,"textAnchor":88},"234","2 find",[82,132,133],{"x":129,"y":108,"style":109,"fill":80,"textAnchor":88},"layers where",[82,135,137],{"x":129,"y":113,"style":136,"fill":80,"textAnchor":88},"text-anchor:middle;font-size:9.5px;font-family:monospace","not isValid()",[116,139],{"x1":62,"y1":119,"x2":140,"y2":119,"stroke":80,"style":121},"314",[58,142],{"x":143,"y":93,"width":94,"height":95,"rx":96,"fill":144,"stroke":145,"style":99},"320","#eff3ff","#2563eb",[82,147,149],{"x":148,"y":103,"style":104,"fill":145,"textAnchor":88},"386","3 decode",[82,151,152],{"x":148,"y":108,"style":109,"fill":80,"textAnchor":88},"source into",[82,154,155],{"x":148,"y":113,"style":109,"fill":80,"textAnchor":88},"path + options",[116,157],{"x1":158,"y1":119,"x2":159,"y2":119,"stroke":80,"style":121},"452","466",[58,161],{"x":162,"y":93,"width":94,"height":95,"rx":96,"fill":125,"stroke":163,"style":99},"472","#b45309",[82,165,167],{"x":166,"y":103,"style":104,"fill":163,"textAnchor":88},"538","4 remap",[82,169,170],{"x":166,"y":108,"style":109,"fill":80,"textAnchor":88},"old prefix to",[82,172,173],{"x":166,"y":113,"style":109,"fill":80,"textAnchor":88},"new prefix",[116,175],{"x1":176,"y1":119,"x2":177,"y2":119,"stroke":80,"style":121},"604","618",[58,179],{"x":180,"y":93,"width":181,"height":95,"rx":96,"fill":182,"stroke":183,"style":99},"624","120","#e8efe6","#15803d",[82,185,188],{"x":186,"y":103,"style":104,"fill":187,"textAnchor":88},"684","#166534","5 apply",[82,190,191],{"x":186,"y":108,"style":136,"fill":80,"textAnchor":88},"setDataSource",[82,193,194],{"x":186,"y":113,"style":109,"fill":80,"textAnchor":88},"then write()",[58,196],{"x":181,"y":197,"width":198,"height":199,"rx":96,"fill":200,"stroke":201,"style":99},"190","520","84","#fffdf7","#59645f",[82,203,205],{"x":84,"y":204,"style":104,"fill":87,"textAnchor":88},"216","mapping table",[82,207,210],{"x":84,"y":208,"style":209,"fill":80,"textAnchor":88},"240","text-anchor:middle;font-size:10px;font-family:monospace","S:\u002Fprojects\u002F  →  \u002F\u002Ffileserver\u002Fgis\u002Fprojects\u002F",[82,212,214],{"x":84,"y":213,"style":209,"fill":80,"textAnchor":88},"260","host=db-old  →  host=db-new",[216,217,219],"h2",{"id":218},"prerequisites","Prerequisites",[221,222,223,231,234],"ul",{},[224,225,226,230],"li",{},[227,228,229],"strong",{},"QGIS 3.40 LTR"," or newer; the flags shown also exist on 3.28 and later under their older names.",[224,232,233],{},"A copy of the projects to repair. Write the fixed files next to the originals or into a new folder until you trust the script.",[224,235,236],{},"Knowing where the data went. A script can apply a mapping; it cannot discover one.",[216,238,240],{"id":239},"open-a-project-without-loading-its-layers","Open a project without loading its layers",[14,242,243,244,247],{},"Reading a project normally makes every provider try to open its source, which for a missing network share means a timeout per layer and, on some platforms, a modal dialog. The ",[18,245,246],{},"DontResolveLayers"," flag reads the layer definitions and creates placeholder layers that keep their source strings but never touch the data.",[249,250,255],"pre",{"className":251,"code":252,"language":253,"meta":254,"style":254},"language-python shiki shiki-themes github-dark","from qgis.core import Qgis, QgsProject\n\nproject = QgsProject.instance()\nproject.clear()\nok = project.read(\n    \"\u002Fsrv\u002Fprojects\u002Fflood_2025.qgz\",\n    Qgis.ProjectReadFlag.DontResolveLayers,\n)\nif not ok:\n    raise RuntimeError(project.error())\n\nbroken = [lyr for lyr in project.mapLayers().values() if not lyr.isValid()]\nprint(f\"{len(broken)} of {len(project.mapLayers())} layers unresolved\")\n","python","",[18,256,257,275,282,294,300,311,321,327,333,345,358,363,393],{"__ignoreMap":254},[258,259,261,265,269,272],"span",{"class":116,"line":260},1,[258,262,264],{"class":263},"snl16","from",[258,266,268],{"class":267},"s95oV"," qgis.core ",[258,270,271],{"class":263},"import",[258,273,274],{"class":267}," Qgis, QgsProject\n",[258,276,278],{"class":116,"line":277},2,[258,279,281],{"emptyLinePlaceholder":280},true,"\n",[258,283,285,288,291],{"class":116,"line":284},3,[258,286,287],{"class":267},"project ",[258,289,290],{"class":263},"=",[258,292,293],{"class":267}," QgsProject.instance()\n",[258,295,297],{"class":116,"line":296},4,[258,298,299],{"class":267},"project.clear()\n",[258,301,303,306,308],{"class":116,"line":302},5,[258,304,305],{"class":267},"ok ",[258,307,290],{"class":263},[258,309,310],{"class":267}," project.read(\n",[258,312,314,318],{"class":116,"line":313},6,[258,315,317],{"class":316},"sU2Wk","    \"\u002Fsrv\u002Fprojects\u002Fflood_2025.qgz\"",[258,319,320],{"class":267},",\n",[258,322,324],{"class":116,"line":323},7,[258,325,326],{"class":267},"    Qgis.ProjectReadFlag.DontResolveLayers,\n",[258,328,330],{"class":116,"line":329},8,[258,331,332],{"class":267},")\n",[258,334,336,339,342],{"class":116,"line":335},9,[258,337,338],{"class":263},"if",[258,340,341],{"class":263}," not",[258,343,344],{"class":267}," ok:\n",[258,346,348,351,355],{"class":116,"line":347},10,[258,349,350],{"class":263},"    raise",[258,352,354],{"class":353},"sDLfK"," RuntimeError",[258,356,357],{"class":267},"(project.error())\n",[258,359,361],{"class":116,"line":360},11,[258,362,281],{"emptyLinePlaceholder":280},[258,364,366,369,371,374,377,380,383,386,388,390],{"class":116,"line":365},12,[258,367,368],{"class":267},"broken ",[258,370,290],{"class":263},[258,372,373],{"class":267}," [lyr ",[258,375,376],{"class":263},"for",[258,378,379],{"class":267}," lyr ",[258,381,382],{"class":263},"in",[258,384,385],{"class":267}," project.mapLayers().values() ",[258,387,338],{"class":263},[258,389,341],{"class":263},[258,391,392],{"class":267}," lyr.isValid()]\n",[258,394,396,399,402,405,408,411,414,417,420,422,425,427,430],{"class":116,"line":395},13,[258,397,398],{"class":353},"print",[258,400,401],{"class":267},"(",[258,403,404],{"class":263},"f",[258,406,407],{"class":316},"\"",[258,409,410],{"class":353},"{len",[258,412,413],{"class":267},"(broken)",[258,415,416],{"class":353},"}",[258,418,419],{"class":316}," of ",[258,421,410],{"class":353},[258,423,424],{"class":267},"(project.mapLayers())",[258,426,416],{"class":353},[258,428,429],{"class":316}," layers unresolved\"",[258,431,332],{"class":267},[14,433,434,437,438,441],{},[227,435,436],{},"Breakdown:"," Placeholder layers are all invalid by construction, which is fine for this purpose — the question is not whether they opened but where they point. Style, labels, joins and layout references are read as normal, so everything that should survive the repair is already attached to the layer objects. ",[18,439,440],{},"project.clear()"," first matters in the Python console, where the singleton otherwise still holds whatever project was open before.",[14,443,444],{},"To tell a genuinely broken layer from one that was merely not resolved, check the file:",[249,446,448],{"className":251,"code":447,"language":253,"meta":254,"style":254},"import os\nfrom qgis.core import QgsProviderRegistry\n\nregistry = QgsProviderRegistry.instance()\nmissing = []\nfor layer in project.mapLayers().values():\n    parts = registry.decodeUri(layer.providerType(), layer.source())\n    path = parts.get(\"path\")\n    if path and not os.path.exists(path):\n        missing.append((layer, parts))\n\nfor layer, parts in missing:\n    print(f\"{layer.name():\u003C30} {parts['path']}\")\n",[18,449,450,457,468,472,482,492,504,514,529,545,550,554,566],{"__ignoreMap":254},[258,451,452,454],{"class":116,"line":260},[258,453,271],{"class":263},[258,455,456],{"class":267}," os\n",[258,458,459,461,463,465],{"class":116,"line":277},[258,460,264],{"class":263},[258,462,268],{"class":267},[258,464,271],{"class":263},[258,466,467],{"class":267}," QgsProviderRegistry\n",[258,469,470],{"class":116,"line":284},[258,471,281],{"emptyLinePlaceholder":280},[258,473,474,477,479],{"class":116,"line":296},[258,475,476],{"class":267},"registry ",[258,478,290],{"class":263},[258,480,481],{"class":267}," QgsProviderRegistry.instance()\n",[258,483,484,487,489],{"class":116,"line":302},[258,485,486],{"class":267},"missing ",[258,488,290],{"class":263},[258,490,491],{"class":267}," []\n",[258,493,494,496,499,501],{"class":116,"line":313},[258,495,376],{"class":263},[258,497,498],{"class":267}," layer ",[258,500,382],{"class":263},[258,502,503],{"class":267}," project.mapLayers().values():\n",[258,505,506,509,511],{"class":116,"line":323},[258,507,508],{"class":267},"    parts ",[258,510,290],{"class":263},[258,512,513],{"class":267}," registry.decodeUri(layer.providerType(), layer.source())\n",[258,515,516,519,521,524,527],{"class":116,"line":329},[258,517,518],{"class":267},"    path ",[258,520,290],{"class":263},[258,522,523],{"class":267}," parts.get(",[258,525,526],{"class":316},"\"path\"",[258,528,332],{"class":267},[258,530,531,534,537,540,542],{"class":116,"line":335},[258,532,533],{"class":263},"    if",[258,535,536],{"class":267}," path ",[258,538,539],{"class":263},"and",[258,541,341],{"class":263},[258,543,544],{"class":267}," os.path.exists(path):\n",[258,546,547],{"class":116,"line":347},[258,548,549],{"class":267},"        missing.append((layer, parts))\n",[258,551,552],{"class":116,"line":360},[258,553,281],{"emptyLinePlaceholder":280},[258,555,556,558,561,563],{"class":116,"line":365},[258,557,376],{"class":263},[258,559,560],{"class":267}," layer, parts ",[258,562,382],{"class":263},[258,564,565],{"class":267}," missing:\n",[258,567,568,571,573,575,577,580,583,586,588,591,594,597,600,602,604],{"class":116,"line":395},[258,569,570],{"class":353},"    print",[258,572,401],{"class":267},[258,574,404],{"class":263},[258,576,407],{"class":316},[258,578,579],{"class":353},"{",[258,581,582],{"class":267},"layer.name()",[258,584,585],{"class":263},":\u003C30",[258,587,416],{"class":353},[258,589,590],{"class":353}," {",[258,592,593],{"class":267},"parts[",[258,595,596],{"class":316},"'path'",[258,598,599],{"class":267},"]",[258,601,416],{"class":353},[258,603,407],{"class":316},[258,605,332],{"class":267},[14,607,608,610,611,613,614,617,618,622,623,626],{},[227,609,436],{}," Only file-based providers return a ",[18,612,77],{},", so database and web layers are skipped here and handled separately below. ",[18,615,616],{},"os.path.exists"," is quick against a local disk and slow against a dead network share; on a large batch, check each distinct directory once and cache the answer rather than stat-ing every layer. The decoding is the technique from ",[35,619,621],{"href":620},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fdecode-and-build-data-source-uris-pyqgis\u002F","decoding data source URIs",", and it is what makes this safe for GeoPackage layers whose sources carry ",[18,624,625],{},"|layername="," suffixes.",[216,628,630],{"id":629},"remap-paths-with-a-table","Remap paths with a table",[14,632,633],{},"Migrations almost always move a prefix: a drive letter becomes a UNC path, a user's home directory becomes a shared folder. An ordered list of prefix pairs handles that and handles exceptions, as long as the longest prefix is tried first.",[14,635,636],{},[43,637,640,643,646,649,652,657,662,668,672,677,682,687,690,694,698,701,705,709,713,718,722],{"viewBox":638,"role":46,"ariaLabel":639,"xmlns":48},"0 0 760 272","Longest prefix wins: a path that matches both a general and a specific rule takes the specific one, and a path that matches no rule is reported rather than guessed",[50,641,642],{},"Order the mapping from specific to general",[54,644,645],{},"Three sample paths are tested against two rules. The rule for S:\u002Fprojects\u002Farchive\u002F is more specific than the rule for S:\u002Fprojects\u002F. The archive path takes the specific rule. An ordinary project path takes the general rule. A path on C: matches neither and is reported as unmapped instead of being changed.",[58,647],{"x":60,"y":60,"width":61,"height":648,"fill":63},"272",[82,650,651],{"x":84,"y":85,"style":86,"fill":87,"textAnchor":88},"Longest matching prefix wins; no match means report",[58,653],{"x":654,"y":655,"width":656,"height":95,"rx":96,"fill":200,"stroke":201,"style":99},"24","50","330",[82,658,661],{"x":659,"y":660,"style":104,"fill":87,"textAnchor":88},"189","74","rules, tried in this order",[82,663,667],{"x":664,"y":665,"style":666,"fill":145},"44","102","font-size:9.5px;font-family:monospace","1  S:\u002Fprojects\u002Farchive\u002F → \u002F\u002Farch\u002Fgis\u002F",[82,669,671],{"x":664,"y":670,"style":666,"fill":98},"126","2  S:\u002Fprojects\u002F → \u002F\u002Ffileserver\u002Fgis\u002F",[58,673],{"x":674,"y":655,"width":675,"height":676,"rx":96,"fill":144,"stroke":145,"style":99},"378","358","62",[82,678,681],{"x":679,"y":680,"style":666,"fill":80},"398","76","S:\u002Fprojects\u002Farchive\u002F1998.shp",[82,683,686],{"x":679,"y":684,"style":685,"fill":145},"98","font-size:9.5px;font-family:sans-serif","rule 1 → \u002F\u002Farch\u002Fgis\u002F1998.shp",[58,688],{"x":674,"y":689,"width":675,"height":676,"rx":96,"fill":97,"stroke":98,"style":99},"124",[82,691,693],{"x":679,"y":692,"style":666,"fill":80},"150","S:\u002Fprojects\u002Fflood\u002Fextent.gpkg",[82,695,697],{"x":679,"y":696,"style":685,"fill":98},"172","rule 2 → \u002F\u002Ffileserver\u002Fgis\u002Fflood\u002Fextent.gpkg",[58,699],{"x":674,"y":700,"width":675,"height":676,"rx":96,"fill":125,"stroke":126,"style":99},"198",[82,702,704],{"x":679,"y":703,"style":666,"fill":80},"224","C:\u002FUsers\u002Famy\u002FDesktop\u002Ftemp.shp",[82,706,708],{"x":679,"y":707,"style":685,"fill":126},"246","no rule → listed as unmapped",[58,710],{"x":654,"y":711,"width":656,"height":712,"rx":96,"fill":125,"stroke":163,"style":99},"166","94",[82,714,717],{"x":659,"y":715,"style":716,"fill":80,"textAnchor":88},"194","text-anchor:middle;font-size:10.5px;font-family:sans-serif","tried the other way round, rule 2",[82,719,721],{"x":659,"y":720,"style":716,"fill":80,"textAnchor":88},"214","would swallow the archive path",[82,723,725],{"x":659,"y":208,"style":724,"fill":163,"textAnchor":88},"text-anchor:middle;font-size:10px;font-family:sans-serif","sort by prefix length, longest first",[249,727,729],{"className":251,"code":728,"language":253,"meta":254,"style":254},"from qgis.core import QgsDataProvider\n\nPATH_MAP = {\n    \"S:\u002Fprojects\u002Farchive\u002F\": \"\u002F\u002Farch\u002Fgis\u002F\",\n    \"S:\u002Fprojects\u002F\": \"\u002F\u002Ffileserver\u002Fgis\u002Fprojects\u002F\",\n}\nrules = sorted(PATH_MAP.items(), key=lambda kv: len(kv[0]), reverse=True)\n\ndef remap(path):\n    norm = path.replace(\"\\\\\", \"\u002F\")\n    for old, new in rules:\n        if norm.lower().startswith(old.lower()):\n            return new + norm[len(old):]\n    return None\n\noptions = QgsDataProvider.ProviderOptions()\noptions.transformContext = project.transformContext()\n\nunmapped = []\nfor layer, parts in missing:\n    new_path = remap(parts[\"path\"])\n    if new_path is None or not os.path.exists(new_path):\n        unmapped.append((layer.name(), parts[\"path\"], new_path))\n        continue\n    parts[\"path\"] = new_path\n    source = registry.encodeUri(layer.providerType(), parts)\n    layer.setDataSource(source, layer.name(), layer.providerType(), options)\n\nfor row in unmapped:\n    print(\"UNMAPPED\", *row)\n",[18,730,731,742,746,757,770,782,787,835,839,851,876,889,897,916,925,930,941,952,957,967,978,994,1016,1027,1033,1049,1060,1066,1071,1084],{"__ignoreMap":254},[258,732,733,735,737,739],{"class":116,"line":260},[258,734,264],{"class":263},[258,736,268],{"class":267},[258,738,271],{"class":263},[258,740,741],{"class":267}," QgsDataProvider\n",[258,743,744],{"class":116,"line":277},[258,745,281],{"emptyLinePlaceholder":280},[258,747,748,751,754],{"class":116,"line":284},[258,749,750],{"class":353},"PATH_MAP",[258,752,753],{"class":263}," =",[258,755,756],{"class":267}," {\n",[258,758,759,762,765,768],{"class":116,"line":296},[258,760,761],{"class":316},"    \"S:\u002Fprojects\u002Farchive\u002F\"",[258,763,764],{"class":267},": ",[258,766,767],{"class":316},"\"\u002F\u002Farch\u002Fgis\u002F\"",[258,769,320],{"class":267},[258,771,772,775,777,780],{"class":116,"line":302},[258,773,774],{"class":316},"    \"S:\u002Fprojects\u002F\"",[258,776,764],{"class":267},[258,778,779],{"class":316},"\"\u002F\u002Ffileserver\u002Fgis\u002Fprojects\u002F\"",[258,781,320],{"class":267},[258,783,784],{"class":116,"line":313},[258,785,786],{"class":267},"}\n",[258,788,789,792,794,797,799,801,804,808,811,814,817,820,822,825,828,830,833],{"class":116,"line":323},[258,790,791],{"class":267},"rules ",[258,793,290],{"class":263},[258,795,796],{"class":353}," sorted",[258,798,401],{"class":267},[258,800,750],{"class":353},[258,802,803],{"class":267},".items(), ",[258,805,807],{"class":806},"s9osk","key",[258,809,810],{"class":263},"=lambda",[258,812,813],{"class":267}," kv: ",[258,815,816],{"class":353},"len",[258,818,819],{"class":267},"(kv[",[258,821,60],{"class":353},[258,823,824],{"class":267},"]), ",[258,826,827],{"class":806},"reverse",[258,829,290],{"class":263},[258,831,832],{"class":353},"True",[258,834,332],{"class":267},[258,836,837],{"class":116,"line":329},[258,838,281],{"emptyLinePlaceholder":280},[258,840,841,844,848],{"class":116,"line":335},[258,842,843],{"class":263},"def",[258,845,847],{"class":846},"svObZ"," remap",[258,849,850],{"class":267},"(path):\n",[258,852,853,856,858,861,863,866,868,871,874],{"class":116,"line":347},[258,854,855],{"class":267},"    norm ",[258,857,290],{"class":263},[258,859,860],{"class":267}," path.replace(",[258,862,407],{"class":316},[258,864,865],{"class":353},"\\\\",[258,867,407],{"class":316},[258,869,870],{"class":267},", ",[258,872,873],{"class":316},"\"\u002F\"",[258,875,332],{"class":267},[258,877,878,881,884,886],{"class":116,"line":360},[258,879,880],{"class":263},"    for",[258,882,883],{"class":267}," old, new ",[258,885,382],{"class":263},[258,887,888],{"class":267}," rules:\n",[258,890,891,894],{"class":116,"line":365},[258,892,893],{"class":263},"        if",[258,895,896],{"class":267}," norm.lower().startswith(old.lower()):\n",[258,898,899,902,905,908,911,913],{"class":116,"line":395},[258,900,901],{"class":263},"            return",[258,903,904],{"class":267}," new ",[258,906,907],{"class":263},"+",[258,909,910],{"class":267}," norm[",[258,912,816],{"class":353},[258,914,915],{"class":267},"(old):]\n",[258,917,919,922],{"class":116,"line":918},14,[258,920,921],{"class":263},"    return",[258,923,924],{"class":353}," None\n",[258,926,928],{"class":116,"line":927},15,[258,929,281],{"emptyLinePlaceholder":280},[258,931,933,936,938],{"class":116,"line":932},16,[258,934,935],{"class":267},"options ",[258,937,290],{"class":263},[258,939,940],{"class":267}," QgsDataProvider.ProviderOptions()\n",[258,942,944,947,949],{"class":116,"line":943},17,[258,945,946],{"class":267},"options.transformContext ",[258,948,290],{"class":263},[258,950,951],{"class":267}," project.transformContext()\n",[258,953,955],{"class":116,"line":954},18,[258,956,281],{"emptyLinePlaceholder":280},[258,958,960,963,965],{"class":116,"line":959},19,[258,961,962],{"class":267},"unmapped ",[258,964,290],{"class":263},[258,966,491],{"class":267},[258,968,970,972,974,976],{"class":116,"line":969},20,[258,971,376],{"class":263},[258,973,560],{"class":267},[258,975,382],{"class":263},[258,977,565],{"class":267},[258,979,981,984,986,989,991],{"class":116,"line":980},21,[258,982,983],{"class":267},"    new_path ",[258,985,290],{"class":263},[258,987,988],{"class":267}," remap(parts[",[258,990,526],{"class":316},[258,992,993],{"class":267},"])\n",[258,995,997,999,1002,1005,1008,1011,1013],{"class":116,"line":996},22,[258,998,533],{"class":263},[258,1000,1001],{"class":267}," new_path ",[258,1003,1004],{"class":263},"is",[258,1006,1007],{"class":353}," None",[258,1009,1010],{"class":263}," or",[258,1012,341],{"class":263},[258,1014,1015],{"class":267}," os.path.exists(new_path):\n",[258,1017,1019,1022,1024],{"class":116,"line":1018},23,[258,1020,1021],{"class":267},"        unmapped.append((layer.name(), parts[",[258,1023,526],{"class":316},[258,1025,1026],{"class":267},"], new_path))\n",[258,1028,1030],{"class":116,"line":1029},24,[258,1031,1032],{"class":263},"        continue\n",[258,1034,1036,1039,1041,1044,1046],{"class":116,"line":1035},25,[258,1037,1038],{"class":267},"    parts[",[258,1040,526],{"class":316},[258,1042,1043],{"class":267},"] ",[258,1045,290],{"class":263},[258,1047,1048],{"class":267}," new_path\n",[258,1050,1052,1055,1057],{"class":116,"line":1051},26,[258,1053,1054],{"class":267},"    source ",[258,1056,290],{"class":263},[258,1058,1059],{"class":267}," registry.encodeUri(layer.providerType(), parts)\n",[258,1061,1063],{"class":116,"line":1062},27,[258,1064,1065],{"class":267},"    layer.setDataSource(source, layer.name(), layer.providerType(), options)\n",[258,1067,1069],{"class":116,"line":1068},28,[258,1070,281],{"emptyLinePlaceholder":280},[258,1072,1074,1076,1079,1081],{"class":116,"line":1073},29,[258,1075,376],{"class":263},[258,1077,1078],{"class":267}," row ",[258,1080,382],{"class":263},[258,1082,1083],{"class":267}," unmapped:\n",[258,1085,1087,1089,1091,1094,1096,1099],{"class":116,"line":1086},30,[258,1088,570],{"class":353},[258,1090,401],{"class":267},[258,1092,1093],{"class":316},"\"UNMAPPED\"",[258,1095,870],{"class":267},[258,1097,1098],{"class":263},"*",[258,1100,1101],{"class":267},"row)\n",[14,1103,1104,1106,1107,1110,1111,1113],{},[227,1105,436],{}," Normalising backslashes and comparing case-insensitively matters for Windows paths, where the same folder is written three different ways across a decade of projects. The existence check on the ",[27,1108,1109],{},"new"," path is what stops a wrong rule from quietly replacing one broken path with another. ",[18,1112,191],{}," on a placeholder layer resolves it for real, so after this loop the repaired layers are valid and the unmapped ones are listed for a human.",[216,1115,1117],{"id":1116},"database-hosts-and-services","Database hosts and services",[14,1119,1120,1121,1124,1125,1128,1129,1132],{},"Database layers are repaired the same way, with ",[18,1122,1123],{},"QgsDataSourceUri"," in place of the registry. A host rename is the common case; a move to a ",[18,1126,1127],{},"service="," entry in ",[18,1130,1131],{},"pg_service.conf"," is the better long-term fix, because the next rename then changes one file instead of every project.",[249,1134,1136],{"className":251,"code":1135,"language":253,"meta":254,"style":254},"from qgis.core import QgsDataSourceUri\n\nfor layer in project.mapLayers().values():\n    if layer.providerType() != \"postgres\":\n        continue\n    uri = QgsDataSourceUri(layer.source())\n    if uri.host() == \"db-old.internal\":\n        uri.setConnection(\"db-new.internal\", uri.port(), uri.database(),\n                          \"\", \"\", uri.sslMode(), uri.authConfigId())\n        layer.setDataSource(uri.uri(False), layer.name(), \"postgres\", options)\n",[18,1137,1138,1149,1153,1163,1179,1183,1193,1208,1219,1232],{"__ignoreMap":254},[258,1139,1140,1142,1144,1146],{"class":116,"line":260},[258,1141,264],{"class":263},[258,1143,268],{"class":267},[258,1145,271],{"class":263},[258,1147,1148],{"class":267}," QgsDataSourceUri\n",[258,1150,1151],{"class":116,"line":277},[258,1152,281],{"emptyLinePlaceholder":280},[258,1154,1155,1157,1159,1161],{"class":116,"line":284},[258,1156,376],{"class":263},[258,1158,498],{"class":267},[258,1160,382],{"class":263},[258,1162,503],{"class":267},[258,1164,1165,1167,1170,1173,1176],{"class":116,"line":296},[258,1166,533],{"class":263},[258,1168,1169],{"class":267}," layer.providerType() ",[258,1171,1172],{"class":263},"!=",[258,1174,1175],{"class":316}," \"postgres\"",[258,1177,1178],{"class":267},":\n",[258,1180,1181],{"class":116,"line":302},[258,1182,1032],{"class":263},[258,1184,1185,1188,1190],{"class":116,"line":313},[258,1186,1187],{"class":267},"    uri ",[258,1189,290],{"class":263},[258,1191,1192],{"class":267}," QgsDataSourceUri(layer.source())\n",[258,1194,1195,1197,1200,1203,1206],{"class":116,"line":323},[258,1196,533],{"class":263},[258,1198,1199],{"class":267}," uri.host() ",[258,1201,1202],{"class":263},"==",[258,1204,1205],{"class":316}," \"db-old.internal\"",[258,1207,1178],{"class":267},[258,1209,1210,1213,1216],{"class":116,"line":329},[258,1211,1212],{"class":267},"        uri.setConnection(",[258,1214,1215],{"class":316},"\"db-new.internal\"",[258,1217,1218],{"class":267},", uri.port(), uri.database(),\n",[258,1220,1221,1224,1226,1229],{"class":116,"line":335},[258,1222,1223],{"class":316},"                          \"\"",[258,1225,870],{"class":267},[258,1227,1228],{"class":316},"\"\"",[258,1230,1231],{"class":267},", uri.sslMode(), uri.authConfigId())\n",[258,1233,1234,1237,1240,1243,1246],{"class":116,"line":347},[258,1235,1236],{"class":267},"        layer.setDataSource(uri.uri(",[258,1238,1239],{"class":353},"False",[258,1241,1242],{"class":267},"), layer.name(), ",[258,1244,1245],{"class":316},"\"postgres\"",[258,1247,1248],{"class":267},", options)\n",[14,1250,1251,1253,1254,1257],{},[227,1252,436],{}," Rebuilding the connection with empty user and password, and the existing ",[18,1255,1256],{},"authConfigId()",", keeps credentials out of the project; if the old projects had passwords stored inline, this is also the moment to strip them. Checking the old host explicitly means projects that were already pointed at the new server pass through untouched.",[216,1259,1261],{"id":1260},"dry-run-a-whole-folder-first","Dry-run a whole folder first",[14,1263,1264],{},"A mapping that looks right against one project can be wrong against the fortieth. Before rewriting anything, run the detection and remapping across every project without saving, and collect a report that someone who knows the file server can read.",[249,1266,1268],{"className":251,"code":1267,"language":253,"meta":254,"style":254},"import csv\nfrom pathlib import Path\n\ndef audit(project_path):\n    project = QgsProject.instance()\n    project.clear()\n    project.read(str(project_path), Qgis.ProjectReadFlag.DontResolveLayers)\n    rows = []\n    for layer in project.mapLayers().values():\n        parts = registry.decodeUri(layer.providerType(), layer.source())\n        path = parts.get(\"path\")\n        if not path or os.path.exists(path):\n            continue\n        target = remap(path)\n        status = (\"fixable\" if target and os.path.exists(target)\n                  else \"no rule\" if target is None else \"target missing\")\n        rows.append([project_path.name, layer.name(), path, target or \"\", status])\n    return rows\n\nwith open(\"\u002Fsrv\u002Freports\u002Fpath_audit.csv\", \"w\", newline=\"\") as fh:\n    writer = csv.writer(fh)\n    writer.writerow([\"project\", \"layer\", \"old_path\", \"new_path\", \"status\"])\n    for qgz in sorted(Path(\"\u002Fsrv\u002Fprojects\").rglob(\"*.qg[sz]\")):\n        writer.writerows(audit(qgz))\n",[18,1269,1270,1277,1289,1293,1303,1312,1317,1328,1337,1347,1356,1369,1382,1387,1397,1421,1445,1458,1465,1469,1505,1515,1545,1571],{"__ignoreMap":254},[258,1271,1272,1274],{"class":116,"line":260},[258,1273,271],{"class":263},[258,1275,1276],{"class":267}," csv\n",[258,1278,1279,1281,1284,1286],{"class":116,"line":277},[258,1280,264],{"class":263},[258,1282,1283],{"class":267}," pathlib ",[258,1285,271],{"class":263},[258,1287,1288],{"class":267}," Path\n",[258,1290,1291],{"class":116,"line":284},[258,1292,281],{"emptyLinePlaceholder":280},[258,1294,1295,1297,1300],{"class":116,"line":296},[258,1296,843],{"class":263},[258,1298,1299],{"class":846}," audit",[258,1301,1302],{"class":267},"(project_path):\n",[258,1304,1305,1308,1310],{"class":116,"line":302},[258,1306,1307],{"class":267},"    project ",[258,1309,290],{"class":263},[258,1311,293],{"class":267},[258,1313,1314],{"class":116,"line":313},[258,1315,1316],{"class":267},"    project.clear()\n",[258,1318,1319,1322,1325],{"class":116,"line":323},[258,1320,1321],{"class":267},"    project.read(",[258,1323,1324],{"class":353},"str",[258,1326,1327],{"class":267},"(project_path), Qgis.ProjectReadFlag.DontResolveLayers)\n",[258,1329,1330,1333,1335],{"class":116,"line":329},[258,1331,1332],{"class":267},"    rows ",[258,1334,290],{"class":263},[258,1336,491],{"class":267},[258,1338,1339,1341,1343,1345],{"class":116,"line":335},[258,1340,880],{"class":263},[258,1342,498],{"class":267},[258,1344,382],{"class":263},[258,1346,503],{"class":267},[258,1348,1349,1352,1354],{"class":116,"line":347},[258,1350,1351],{"class":267},"        parts ",[258,1353,290],{"class":263},[258,1355,513],{"class":267},[258,1357,1358,1361,1363,1365,1367],{"class":116,"line":360},[258,1359,1360],{"class":267},"        path ",[258,1362,290],{"class":263},[258,1364,523],{"class":267},[258,1366,526],{"class":316},[258,1368,332],{"class":267},[258,1370,1371,1373,1375,1377,1380],{"class":116,"line":365},[258,1372,893],{"class":263},[258,1374,341],{"class":263},[258,1376,536],{"class":267},[258,1378,1379],{"class":263},"or",[258,1381,544],{"class":267},[258,1383,1384],{"class":116,"line":395},[258,1385,1386],{"class":263},"            continue\n",[258,1388,1389,1392,1394],{"class":116,"line":918},[258,1390,1391],{"class":267},"        target ",[258,1393,290],{"class":263},[258,1395,1396],{"class":267}," remap(path)\n",[258,1398,1399,1402,1404,1407,1410,1413,1416,1418],{"class":116,"line":927},[258,1400,1401],{"class":267},"        status ",[258,1403,290],{"class":263},[258,1405,1406],{"class":267}," (",[258,1408,1409],{"class":316},"\"fixable\"",[258,1411,1412],{"class":263}," if",[258,1414,1415],{"class":267}," target ",[258,1417,539],{"class":263},[258,1419,1420],{"class":267}," os.path.exists(target)\n",[258,1422,1423,1426,1429,1431,1433,1435,1437,1440,1443],{"class":116,"line":932},[258,1424,1425],{"class":263},"                  else",[258,1427,1428],{"class":316}," \"no rule\"",[258,1430,1412],{"class":263},[258,1432,1415],{"class":267},[258,1434,1004],{"class":263},[258,1436,1007],{"class":353},[258,1438,1439],{"class":263}," else",[258,1441,1442],{"class":316}," \"target missing\"",[258,1444,332],{"class":267},[258,1446,1447,1450,1452,1455],{"class":116,"line":943},[258,1448,1449],{"class":267},"        rows.append([project_path.name, layer.name(), path, target ",[258,1451,1379],{"class":263},[258,1453,1454],{"class":316}," \"\"",[258,1456,1457],{"class":267},", status])\n",[258,1459,1460,1462],{"class":116,"line":954},[258,1461,921],{"class":263},[258,1463,1464],{"class":267}," rows\n",[258,1466,1467],{"class":116,"line":959},[258,1468,281],{"emptyLinePlaceholder":280},[258,1470,1471,1474,1477,1479,1482,1484,1487,1489,1492,1494,1496,1499,1502],{"class":116,"line":969},[258,1472,1473],{"class":263},"with",[258,1475,1476],{"class":353}," open",[258,1478,401],{"class":267},[258,1480,1481],{"class":316},"\"\u002Fsrv\u002Freports\u002Fpath_audit.csv\"",[258,1483,870],{"class":267},[258,1485,1486],{"class":316},"\"w\"",[258,1488,870],{"class":267},[258,1490,1491],{"class":806},"newline",[258,1493,290],{"class":263},[258,1495,1228],{"class":316},[258,1497,1498],{"class":267},") ",[258,1500,1501],{"class":263},"as",[258,1503,1504],{"class":267}," fh:\n",[258,1506,1507,1510,1512],{"class":116,"line":980},[258,1508,1509],{"class":267},"    writer ",[258,1511,290],{"class":263},[258,1513,1514],{"class":267}," csv.writer(fh)\n",[258,1516,1517,1520,1523,1525,1528,1530,1533,1535,1538,1540,1543],{"class":116,"line":996},[258,1518,1519],{"class":267},"    writer.writerow([",[258,1521,1522],{"class":316},"\"project\"",[258,1524,870],{"class":267},[258,1526,1527],{"class":316},"\"layer\"",[258,1529,870],{"class":267},[258,1531,1532],{"class":316},"\"old_path\"",[258,1534,870],{"class":267},[258,1536,1537],{"class":316},"\"new_path\"",[258,1539,870],{"class":267},[258,1541,1542],{"class":316},"\"status\"",[258,1544,993],{"class":267},[258,1546,1547,1549,1552,1554,1556,1559,1562,1565,1568],{"class":116,"line":1018},[258,1548,880],{"class":263},[258,1550,1551],{"class":267}," qgz ",[258,1553,382],{"class":263},[258,1555,796],{"class":353},[258,1557,1558],{"class":267},"(Path(",[258,1560,1561],{"class":316},"\"\u002Fsrv\u002Fprojects\"",[258,1563,1564],{"class":267},").rglob(",[258,1566,1567],{"class":316},"\"*.qg[sz]\"",[258,1569,1570],{"class":267},")):\n",[258,1572,1573],{"class":116,"line":1029},[258,1574,1575],{"class":267},"        writer.writerows(audit(qgz))\n",[14,1577,1578,1580,1581,1584,1585,1588,1589,1592,1593,1596,1597,1600,1601,1596,1603,1605],{},[227,1579,436],{}," Three statuses cover every outcome that matters: ",[27,1582,1583],{},"fixable"," rows will be repaired, ",[27,1586,1587],{},"no rule"," rows need a new mapping entry, and ",[27,1590,1591],{},"target missing"," rows mean a rule exists but the data did not move where the rule says — the most useful finding, because it usually exposes a folder that was left behind. The glob picks up both ",[18,1594,1595],{},".qgs"," and ",[18,1598,1599],{},".qgz",". Sorting the paths gives a stable report order, so running the audit again after adding rules produces a diff that shows exactly what changed. Only once the ",[27,1602,1587],{},[27,1604,1591],{}," counts are acceptable is it worth switching the same loop over to the repair code.",[216,1607,1609],{"id":1608},"save-with-relative-paths","Save with relative paths",[14,1611,1612],{},"Before writing, switch path storage to relative. Relative paths are resolved against the project file's own folder, so a project and its data can be moved together without breaking.",[14,1614,1615],{},[43,1616,1619,1622,1625,1628,1635,1638,1644,1650,1653,1658,1662,1667,1670,1674,1677,1679,1682],{"viewBox":1617,"role":46,"ariaLabel":1618,"xmlns":48},"0 0 760 250","Absolute versus relative path storage when a project folder is moved: absolute paths break, relative paths still resolve because they are measured from the project file",[50,1620,1621],{},"Why relative paths survive a move",[54,1623,1624],{},"A folder holding flood.qgz and a data subfolder is moved from one drive to another. With absolute storage the project still points at the old drive and every layer breaks. With relative storage the project points at .\u002Fdata\u002Fextent.gpkg, which resolves correctly in the new location.",[58,1626],{"x":60,"y":60,"width":61,"height":1627,"fill":63},"250",[65,1629,1630],{},[68,1631,1633],{"id":1632,"viewBox":71,"refX":72,"refY":73,"markerWidth":74,"markerHeight":74,"orient":75},"fixRelArrow",[77,1634],{"d":79,"fill":80},[82,1636,1637],{"x":84,"y":85,"style":86,"fill":87,"textAnchor":88},"Move the folder: which projects still open?",[58,1639],{"x":654,"y":1640,"width":1641,"height":1642,"rx":96,"fill":125,"stroke":126,"style":1643},"52","344","176","stroke-width:2.5",[82,1645,1649],{"x":1646,"y":1647,"style":1648,"fill":126,"textAnchor":88},"196","78","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","absolute",[82,1651,1652],{"x":1646,"y":119,"style":209,"fill":80,"textAnchor":88},"S:\u002Fflood\u002Fdata\u002Fextent.gpkg",[116,1654],{"x1":1646,"y1":1655,"x2":1646,"y2":1656,"stroke":80,"style":1657},"122","146","stroke-width:1.8;marker-end:url(#fixRelArrow)",[82,1659,1661],{"x":1646,"y":1660,"style":724,"fill":80,"textAnchor":88},"170","folder now lives on T:",[82,1663,1666],{"x":1646,"y":1664,"style":1665,"fill":126,"textAnchor":88},"202","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","every layer unavailable",[58,1668],{"x":1669,"y":1640,"width":1641,"height":1642,"rx":96,"fill":182,"stroke":183,"style":1643},"392",[82,1671,1673],{"x":1672,"y":1647,"style":1648,"fill":187,"textAnchor":88},"564","relative",[82,1675,1676],{"x":1672,"y":119,"style":209,"fill":80,"textAnchor":88},".\u002Fdata\u002Fextent.gpkg",[116,1678],{"x1":1672,"y1":1655,"x2":1672,"y2":1656,"stroke":80,"style":1657},[82,1680,1681],{"x":1672,"y":1660,"style":724,"fill":80,"textAnchor":88},"resolved from the .qgz location",[82,1683,1684],{"x":1672,"y":1664,"style":1665,"fill":187,"textAnchor":88},"opens unchanged",[249,1686,1688],{"className":251,"code":1687,"language":253,"meta":254,"style":254},"from pathlib import Path\n\nproject.setFilePathStorage(Qgis.FilePathType.Relative)\n\nout = Path(\"\u002Fsrv\u002Fprojects_fixed\") \u002F Path(project.fileName()).name\nif not project.write(str(out)):\n    raise RuntimeError(project.error())\nprint(\"saved\", out)\n",[18,1689,1690,1700,1704,1709,1713,1734,1748,1756],{"__ignoreMap":254},[258,1691,1692,1694,1696,1698],{"class":116,"line":260},[258,1693,264],{"class":263},[258,1695,1283],{"class":267},[258,1697,271],{"class":263},[258,1699,1288],{"class":267},[258,1701,1702],{"class":116,"line":277},[258,1703,281],{"emptyLinePlaceholder":280},[258,1705,1706],{"class":116,"line":284},[258,1707,1708],{"class":267},"project.setFilePathStorage(Qgis.FilePathType.Relative)\n",[258,1710,1711],{"class":116,"line":296},[258,1712,281],{"emptyLinePlaceholder":280},[258,1714,1715,1718,1720,1723,1726,1728,1731],{"class":116,"line":302},[258,1716,1717],{"class":267},"out ",[258,1719,290],{"class":263},[258,1721,1722],{"class":267}," Path(",[258,1724,1725],{"class":316},"\"\u002Fsrv\u002Fprojects_fixed\"",[258,1727,1498],{"class":267},[258,1729,1730],{"class":263},"\u002F",[258,1732,1733],{"class":267}," Path(project.fileName()).name\n",[258,1735,1736,1738,1740,1743,1745],{"class":116,"line":313},[258,1737,338],{"class":263},[258,1739,341],{"class":263},[258,1741,1742],{"class":267}," project.write(",[258,1744,1324],{"class":353},[258,1746,1747],{"class":267},"(out)):\n",[258,1749,1750,1752,1754],{"class":116,"line":323},[258,1751,350],{"class":263},[258,1753,354],{"class":353},[258,1755,357],{"class":267},[258,1757,1758,1760,1762,1765],{"class":116,"line":329},[258,1759,398],{"class":353},[258,1761,401],{"class":267},[258,1763,1764],{"class":316},"\"saved\"",[258,1766,1767],{"class":267},", out)\n",[14,1769,1770,1772,1773,1775,1776,1780],{},[227,1771,436],{}," Relative storage only helps for data that sits on the same drive as the project; a path on another Windows drive letter cannot be expressed relatively and is stored absolute regardless. Writing to a new folder keeps the original as a fallback, and the relative paths are computed against the ",[27,1774,1109],{}," location — so copy the data alongside if the fixed project is meant to be portable. Wrapping the whole sequence in a function that takes a project path and a mapping turns it into a batch tool that can walk a directory tree of projects, with the same ",[35,1777,1779],{"href":1778},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fhandle-errors-and-logging-in-unattended-scripts\u002F","error handling you would give any unattended script",".",[216,1782,1784],{"id":1783},"qgis-version-compatibility","QGIS version compatibility",[14,1786,1787,1790,1791,1794,1795,1596,1798,1801,1802,1805,1806,1808],{},[18,1788,1789],{},"Qgis.ProjectReadFlag.DontResolveLayers"," is the 3.26+ spelling; on 3.10–3.24 use ",[18,1792,1793],{},"QgsProject.FlagDontResolveLayers",". ",[18,1796,1797],{},"setFilePathStorage",[18,1799,1800],{},"Qgis.FilePathType"," arrived in 3.22; earlier releases use ",[18,1803,1804],{},"project.writeEntryBool(\"Paths\", \"\u002FAbsolute\", False)",". On the QGIS 4 series only the scoped enum forms are available. ",[18,1807,191],{}," resolving a placeholder layer works from 3.10 onward.",[216,1810,1812],{"id":1811},"troubleshooting","Troubleshooting",[221,1814,1815,1821,1830,1836,1842],{},[224,1816,1817,1820],{},[227,1818,1819],{},"Reading the project hangs."," The flag was not applied and providers are timing out on a dead share. Check the flag name for your version.",[224,1822,1823,1826,1827,1829],{},[227,1824,1825],{},"Layers are repaired but styles are default."," The project was read normally, a layer failed, and QGIS dropped its renderer. Always read with ",[18,1828,246],{}," for repair work.",[224,1831,1832,1835],{},[227,1833,1834],{},"Paths still absolute after saving."," The data is on a different drive from the project file.",[224,1837,1838,1841],{},[227,1839,1840],{},"Some layers repaired, some not."," Mixed separators or case in the stored paths; normalise before matching.",[224,1843,1844,1847],{},[227,1845,1846],{},"Embedded layers or groups stay broken."," They are defined in another project; repair that project first.",[216,1849,1851],{"id":1850},"conclusion","Conclusion",[14,1853,1854,1855,1857],{},"Open projects with layer resolution off, decode each source, apply an ordered prefix mapping, verify the new path exists, and use ",[18,1856,191],{}," so every layer keeps its styling and references. Save with relative paths so that the next reorganisation of the file server is not a repair job.",[216,1859,1861],{"id":1860},"frequently-asked-questions","Frequently Asked Questions",[14,1863,1864,1867,1868,1871],{},[227,1865,1866],{},"Can I fix projects without QGIS Desktop installed?","\nYes. Run the same code in a standalone PyQGIS script with ",[18,1869,1870],{},"QgsApplication"," initialised; no GUI is needed.",[14,1873,1874,1877,1878,1880],{},[227,1875,1876],{},"Should I edit the .qgs XML directly instead?","\nIt works for simple path swaps, but it bypasses provider-specific encoding and breaks on ",[18,1879,1599],{}," without unzipping. The API route handles every provider consistently.",[14,1882,1883,1886,1887,1890,1891,1893],{},[227,1884,1885],{},"What about rasters and GeoPackage rasters?","\nThey use the ",[18,1888,1889],{},"gdal"," provider and decode to a ",[18,1892,77],{}," the same way; the loop above handles them unchanged.",[14,1895,1896,1899,1900,1904],{},[227,1897,1898],{},"How do I stop users saving absolute paths?","\nSet relative storage in a project template, or enforce it with a ",[35,1901,1903],{"href":1902},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002Frun-python-at-qgis-startup-pyqgis\u002F","startup script"," that connects to the project's write signal.",[216,1906,1908],{"id":1907},"related","Related",[221,1910,1911,1916,1921,1927,1933],{},[224,1912,1913,1915],{},[35,1914,38],{"href":37}," — the guide this recipe belongs to",[224,1917,1918],{},[35,1919,1920],{"href":620},"Decode and Build Data Source URIs in PyQGIS",[224,1922,1923],{},[35,1924,1926],{"href":1925},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fsave-and-load-qgis-project-pyqgis\u002F","Save and Load a QGIS Project in PyQGIS",[224,1928,1929],{},[35,1930,1932],{"href":1931},"\u002Fpyqgis-fundamentals-environment-setup\u002Fdebugging-pyqgis-scripts\u002Fdebug-invalid-layer-loading-pyqgis\u002F","Debug Invalid Layer Loading in PyQGIS",[224,1934,1935],{},[35,1936,1938],{"href":1937},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fconnect-to-postgis-database-pyqgis\u002F","Connect to a PostGIS Database in PyQGIS",[1940,1941,1942],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":254,"searchDepth":277,"depth":277,"links":1944},[1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955],{"id":218,"depth":277,"text":219},{"id":239,"depth":277,"text":240},{"id":629,"depth":277,"text":630},{"id":1116,"depth":277,"text":1117},{"id":1260,"depth":277,"text":1261},{"id":1608,"depth":277,"text":1609},{"id":1783,"depth":277,"text":1784},{"id":1811,"depth":277,"text":1812},{"id":1850,"depth":277,"text":1851},{"id":1860,"depth":277,"text":1861},{"id":1907,"depth":277,"text":1908},"Repair a QGIS project whose data moved — open it without resolving layers, find every unavailable source, remap paths or database hosts in bulk with setDataSource, and save it with relative paths so it does not break again.","md",{"slug":1959,"type":1960,"breadcrumb":1961,"datePublished":1962,"dateModified":1962},"fix-broken-layer-paths-in-project-pyqgis","article","Fix Broken Layer Paths","2026-09-17","\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Ffix-broken-layer-paths-in-project-pyqgis",{"title":5,"description":1956},"spatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Ffix-broken-layer-paths-in-project-pyqgis\u002Findex","LF1YBd9G4ZxZ0HGqd6yeVkQauduf8NdW75arTWqIgOM",1789632908431]