--[[----------------------------------------------------------------------------

WFCatFileConductorConstants.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2022 Adobe Inc.
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

----------------------------------------------------------------------------]]--

--[[

This file defines the file system layout on the devices

----------------------------------------------------------------------------]]--

local WFArguments = require "WFArguments"

local KSPathUtils = require "KSPathUtils"
local WFFileUtils = require "WFFileUtils"
local KSTableUtils = require "KSTableUtils"
local WFPlatformInfo = require "WFPlatformInfo"
local WFCatIndexUtils = require "WFCatIndexUtils"
local WFCrypto = require "WFCrypto"
local WFLogger = require "WFLogger"
local WFTableArchiving = require "WFTableArchiving"
local WFRxHttpClient = require "WFRxHttpClient"


local logger = WFLogger "WFCatFileConductorConstants"

--============================================================================--

local check = assert( WFArguments.check )
local derefOpt = assert( KSTableUtils.derefOpt )

local KSPathUtils_child = assert( KSPathUtils.child )

--------------------------------------------------------------------------------

local function isValidXmpCameraRaw( xmpCameraRaw )
	-- SPAM [matthewj 2016-08-08]: This isn't a rigorous check of the xmpCameraRaw, but it should catch
	-- the most egregious incorrect xmpCameraRaw. xmpCameraRaw can be either a string with the xmp in it
	-- or a table containing the sha256 of the xmp string for external xmp handling
	local xmpCameraRawType = type( xmpCameraRaw )

	if xmpCameraRawType == 'table' then

		return xmpCameraRaw.sha256 ~= nil

	end

	return xmpCameraRawType == 'string'

end

--------------------------------------------------------------------------------

local function renditionContentId( docValue )

	if WFCatIndexUtils.isVideoDocument( docValue ) then

		if docValue._spaceId then

			-- For all shared-with-you assets, we must uniqify the default xmp string
			-- by embedding the revision ID as a comment. This allows the downloaders
			-- to correctly invalidate stale renditions when Oz updates the rendition links.

			return docValue._revId or docValue._newRevId

		end


		-- WFTableArchiving.tableToString sorts the keys lexicographically, so this should be
		-- robust against key reordering issues
		local develop = derefOpt( docValue, 'develop' )
		
		local developStr = develop and WFTableArchiving.tableToString( develop ) or ""

		-- Another strategy suggested by @ylo would be to just use userUpdated from adjustments and temporal
		-- (they are supposed to be updated by the client) but this scheme is a bit more robust in case of client bugs
		return WFCrypto.MD5.digest( developStr )

	else

		local xmpCameraRaw = derefOpt( docValue, "develop", "xmpCameraRaw" )

		local userOrientation = derefOpt( docValue, "develop", "userOrientation" )

		-- SPAM [matthewj 2015-12-21]: Ran into issues where the xmpCameraRaw
		-- contained an invalid, non-string value (the image's width value, in
		-- fact) and the md5 calculation would then blow up. So if it's nil or not
		-- a string, then use the default <xmp />

		local defaultXmp = "<xmp />"

		if docValue._spaceId then

			-- For all shared-with-you assets, we must uniqify the default xmp string
			-- by embedding the revision ID as a comment. This allows the downloaders
			-- to correctly invalidate stale renditions when Oz updates the rendition links.

			local uniqueId = docValue._revId or docValue._newRevId

			defaultXmp = string.format( "<xmp><--%s--></xmp>", uniqueId )

		end

		xmpCameraRaw = isValidXmpCameraRaw( xmpCameraRaw ) and xmpCameraRaw or defaultXmp

		local xmpCameraRawForDigest = type( xmpCameraRaw ) == 'table'
									and xmpCameraRaw.sha256
									or xmpCameraRaw

		-- Include SHA256 of the parent binary used to create the renders so that we will
		-- generate new renders when the binary's SHA256 changes due to destructive edits.

		local assetSha256 = derefOpt( docValue, 'importSource', 'sha256' )

		if not assetSha256 then
			-- importSource.sha256 may be missing in rare cases, fall back
			-- to importSource.importTimestamp.
			-- LriOS: see THAssetUtils::GetRenditionContentId()
			assetSha256 = derefOpt( docValue, 'importSource', 'importTimestamp' )
		end

		-- Include the orientation in the digest so changes to it will kick off new renders.

		local compositeDevSettingsForDigest =
				string.format( "%s%s%s",
						xmpCameraRawForDigest,
						userOrientation or 1,
						assetSha256 or '' )

		return WFCrypto.MD5.digest( compositeDevSettingsForDigest )

	end

end

--[[!
	\brief construct a file path for the standard originalFolderDocuments path, based
	on a catalog ID. This is the standard base for relative paths.

	\param catalogId the GUID id for the catalog
	\return (string) the file path
--]]
local function getCatalogBasePath( catalogId )

	WFArguments.check( "+string" )

	local documentsDir = KSPathUtils.getStandardFilePath 'carouselDocuments'

	return KSPathUtils_child( documentsDir, catalogId )

end

--[[!
	\brief construct a file path for the standard originalFolderCaches path, based
	on a catalog ID

	\param docId the document id
	\param catalogIdForCreation if set, the intermediate directory should be created
	\return (string) the file path
--]]
local function getRelativeBlobPath( docId, catalogIdForPathCreation )

	WFArguments.check( "+string", "?string" )

	-- construct a two-level hierarchy ab/ab1234deadbeef, so that not all assetIds are in one directory
	local path = KSPathUtils_child( 'blobs', string.sub( docId, 1, 2 ) )
	path = KSPathUtils_child( path, docId )

	if catalogIdForPathCreation then
		local absPath = KSPathUtils_child( getCatalogBasePath( catalogIdForPathCreation ), path )

		if not pcall( WFFileUtils.mkdir, absPath ) then
			logger:errorf( "could not create asset directory %s for %s", absPath, docId )

			return nil
		end
	end

	return path
end

--------------------------------------------------------------------------------

--[[!
	\brief construct a file path for an original based on a originalsFolderDocuments
	path and a capture date in format like this: 2012-10-08T14:15:44

	\param originalsFolderDocuments Absolute path to the originalFolderDocuments folder
	\param captureDate Date of the original asset that you're trying to find a home for
]]--

local function originalsFolderForDate( originalsFolderDocuments, captureDate )

	WFArguments.check( "string", "string" )

	captureDate = captureDate:sub( 1, 10 )

	local folder = KSPathUtils_child( originalsFolderDocuments, captureDate:sub( 1, 4 ) ) -- yyyy
	folder = KSPathUtils_child( folder, captureDate:sub( 1, 7 ) ) -- yyyy-mm
	folder = KSPathUtils_child( folder, captureDate ) -- yyyy-mm-dd

	return folder

end

--------------------------------------------------------------------------------

--[[!
	\brief Return the relative fs path for a rendition

	\param self A table (see the subtables in \c layoutConventions)
	\param strippedDocValue A (potentially stripped) docValue for the asset
	\param contentId The contentId for the docValue, precomputed by  \c self.contentIdFn (if set)
	\param fileSpec (WFCatFileSpec) the fileSpec for the catalog

	\return A relative path.
--]]
local function renditionPath_mobile( self, strippedDocValue, contentId )

	check( 'table', 'table', 'string' )

	assert( self.prefix, 'missing prefix' )

	local basePath = getRelativeBlobPath( strippedDocValue._id, self._catalogId )

	local fileName = self.prefix .. contentId

	if strippedDocValue.subtype == 'image' then
		fileName = fileName .. '.jpg'
	else
		fileName = fileName .. '.mp4'
	end

	return KSPathUtils_child( basePath, fileName )

end

--------------------------------------------------------------------------------

local function proxyOrOriginalPath_mobile( self, strippedDocValue )

	check( 'table', 'table' )

	local captureDate = derefOpt( strippedDocValue, 'captureDate' ) or '0000-00-00T00:00:00Z'

	local captureTime = captureDate:sub( 12, 19 )

	local basePath = originalsFolderForDate( 'Originals', captureDate )

	local fileName =  derefOpt( strippedDocValue, 'importSource', 'fileName' ) or captureTime

	-- TODO [immisch 2021-12-14]: in case of dots within filenames, an rfind would be better here
	local pos = fileName:find( '.', 1, true )

	if pos then
		local baseFileName = fileName:sub( 1, pos - 1 )
		local ext = fileName:sub( pos )

		if self.suffix then
			fileName = baseFileName .. '_' .. strippedDocValue._id .. self.suffix
		else
			fileName = baseFileName .. '_' .. strippedDocValue._id .. ext
		end

	else
		if self.suffix then
			fileName = fileName .. '_' .. strippedDocValue._id .. self.suffix
		else
			fileName = fileName .. '_' .. strippedDocValue._id
		end
	end

	return KSPathUtils_child( basePath, fileName )

end

--------------------------------------------------------------------------------

local function xmpDevelopPath_mobile( self, strippedDocValue, contentId )

	check( 'table', 'table', 'string' )

	local sha256 = derefOpt( strippedDocValue, 'develop', 'xmpCameraRaw', 'sha256' ) or contentId

	-- construct a two-level hierarchy ab/ab1234deadbeef, so that not all xmpCameraRaw hashes are in one directory
	local path = KSPathUtils_child( 'xmp_develop', string.sub( sha256, 1, 2 ) )
	return KSPathUtils_child( path, sha256 .. '.rdf' )

end

--------------------------------------------------------------------------------

local WFCatFileConductorConstants = {}

WFCatFileConductorConstants.kInvalidUnsynced = 1
WFCatFileConductorConstants.kInvalidLocally = 2
WFCatFileConductorConstants.kInvalidOnOz = 4

WFCatFileConductorConstants.renditionContentId = renditionContentId

if WFPlatformInfo.isMobile() then

	WFCatFileConductorConstants.layoutConventions = {

		basePathFn = function( catalog )
			return getCatalogBasePath( catalog:ozCatalogSpec():catalogId() )
		end,
		[ '/rels/rendition_type/thumbnail2x' ] = {
			shortName = 'thumbnail2x',
			localOnly = { '_localOnly', 'files', 'renditions', 'thumbnail2x' },
			prefix = 'th_',
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = renditionPath_mobile,
		},
		[ '/rels/rendition_type/2048' ] = {
			shortName = 'preview',
			localOnly = { '_localOnly', 'files', 'renditions', '2048' },
			prefix = 'pv_',
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = renditionPath_mobile,
		},
		[ '/rels/rendition_type/360p' ] = {
			shortName = '360p',
			localOnly = { '_localOnly', 'files', 'renditions', '360p' },
			prefix = '360p_',
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = renditionPath_mobile,
		},
		[ '/rels/rendition_type/1080p' ] = {
			shortName = '1080p',
			localOnly = { '_localOnly', 'files', 'renditions', '1080p' },
			prefix = '1080p_',
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = renditionPath_mobile,
		},
		[ '/rels/rendition_type/1080p709' ] = {
			shortName = '1080p709',
			localOnly = { '_localOnly', 'files', 'renditions', '720p' },
			prefix = '1080p709_',
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = renditionPath_mobile,
		},
		[ '/rels/avatar/medium' ] = {
			shortName = 'avatar',
			localOnly = { '_localOnly', 'files', 'renditions', 'avatar' },
			prefix = 'av_',
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = renditionPath_mobile,
		},
		[ '/rels/proxy_type/2560' ] = {
			shortName = 'proxy_2560',
			localOnly = { '_localOnly', 'files', 'proxy', '2560' },
			suffix = '_proxy.dng',
			downloadCost = WFRxHttpClient.requestCostDownloadNegative,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'importSource', 'sha256' )
			end,
			relPathFn = proxyOrOriginalPath_mobile,
		},
		[ '/rels/proxy_type/360p' ] = {
			shortName = 'proxy_360p',
			localOnly = { '_localOnly', 'files', 'proxy', '360p' },
			-- TODO [immisch 2022-03-24]: verify the extension really should be mp4
			suffix = '_360p.mp4',
			downloadCost = WFRxHttpClient.requestCostDownloadNegative,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'importSource', 'sha256' )
			end,
			relPathFn = proxyOrOriginalPath_mobile,
		},
		-- TODO [immisch 2022-03-22]: treating this as a proxy for now (i.e. something that is not regenerated when develop settings change)
		[ '/rels/rendition_type/720p' ] = {
			shortName = 'proxy_720p',
			localOnly = { '_localOnly', 'files', 'proxy', '720p' },
			suffix = '_720p.mp4',
			downloadCost = WFRxHttpClient.requestCostDownloadNegative,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'importSource', 'sha256' )
			end,
			relPathFn = proxyOrOriginalPath_mobile,
		},
		[ '/rels/proxy_type/1080p' ] = {
			shortName = 'proxy_1080p',
			localOnly = { '_localOnly', 'files', 'proxy', '1080p' },
			suffix = '_1080p.mp4',
			downloadCost = WFRxHttpClient.requestCostDownloadNegative,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'importSource', 'sha256' )
			end,
			relPathFn = proxyOrOriginalPath_mobile,
		},
		[ '/rels/proxy_type/1080p709' ] = {
			shortName = 'proxy_1080p709',
			localOnly = { '_localOnly', 'files', 'proxy', '1080p' },
			suffix = '_1080p709.mp4',
			downloadCost = WFRxHttpClient.requestCostDownloadNegative,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'importSource', 'sha256' )
			end,
			relPathFn = proxyOrOriginalPath_mobile,
		},
		[ '/rels/master' ] = {
			shortName = 'original',
			localOnly = { '_localOnly', 'files', 'original', 'main' },
			downloadCost = WFRxHttpClient.requestCostDownloadNegative,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'importSource', 'sha256' )
			end,
			relPathFn = proxyOrOriginalPath_mobile,
		},
		[ '/rels/xmp/develop' ] = {
			shortName = 'xmp_develop',
			localOnly = { '_localOnly', 'files', 'xmp_develop' },
			downloadCost = WFRxHttpClient.requestCostDownloadXmp,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'develop', 'xmpCameraRaw', 'sha256' )
			end,
			relPathFn = xmpDevelopPath_mobile,
		}
	}

else

	-- LrD

	WFCatFileConductorConstants.layoutConventions = {

		basePathFn = function( catalog )
			local wfCatFileSpec = catalog:fileSpec()
			return wfCatFileSpec:catalogPath()
		end,
		[ '/rels/rendition_type/thumbnail2x' ] = {
			shortName = 'thumbnail2x',
			localOnly = { '_localOnly', 'files', 'renditions', 'thumbnail2x' },
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = function( _, docValue, contentId, fileSpec )
				return fileSpec:pathForPreviewDigestAndLongEdge( docValue._id, contentId, 320 )
			end
		},
		[ '/rels/rendition_type/640' ] = {
			shortName = '640',
			localOnly = { '_localOnly', 'files', 'renditions', '640' },
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = function( _, docValue, contentId, fileSpec )
				return fileSpec:pathForPreviewDigestAndLongEdge( docValue._id, contentId, 640 )
			end
		},
		[ '/rels/rendition_type/1280' ] = {
			shortName = '1280',
			localOnly = { '_localOnly', 'files', 'renditions', '1280' },
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = function( _, docValue, contentId, fileSpec )
				return fileSpec:pathForPreviewDigestAndLongEdge( docValue._id, contentId, 1280 )
			end
		},
		[ '/rels/rendition_type/2048' ] = {
			shortName = 'preview',
			localOnly = { '_localOnly', 'files', 'renditions', '2048' },
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = function( _, docValue, contentId, fileSpec )
				return fileSpec:pathForPreviewDigestAndLongEdge( docValue._id, contentId, 2048 )
			end
		},
		[ '/rels/rendition_type/360p' ] = {
			shortName = '360p',
			localOnly = { '_localOnly', 'files', 'renditions', '360p' },
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = function( _, docValue, contentId, fileSpec )
				return fileSpec:pathForPreviewDigestAndLongEdge( docValue._id, contentId, 360 )
			end
		},
		[ '/rels/rendition_type/1080p' ] = {
			shortName = '1080p',
			localOnly = { '_localOnly', 'files', 'renditions', '1080p' },
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = function( _, docValue, contentId, fileSpec )
				return fileSpec:pathForPreviewDigestAndLongEdge( docValue._id, contentId, 1080 )
			end
		},
		[ '/rels/rendition_type/1080p709' ] = {
			shortName = '1080p709',
			localOnly = { '_localOnly', 'files', 'renditions', '1080p709' },
			downloadCost = WFRxHttpClient.requestCostDownloadRendition,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return renditionContentId( docValue )
			end,
			relPathFn = function( _, docValue, contentId, fileSpec )
				return fileSpec:pathForPreviewDigestAndLongEdge( docValue._id, contentId, 1080 )
			end
		},
		[ '/rels/proxy_type/2560' ] = {
			shortName = 'proxy_2560',
			localOnly = { '_localOnly', 'files', 'proxy', '2560' },
			downloadCost = WFRxHttpClient.requestCostDownloadNegative,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'importSource', 'sha256' )
			end,
			relPathFn = function( _, docValue, _, fileSpec )
				return fileSpec:pathForProxy( docValue._id, '2560' )
			end
		},
		[ '/rels/proxy_type/360p' ] = {
			shortName = 'proxy_360p',
			localOnly = { '_localOnly', 'files', 'proxy', '360p' },
			downloadCost = WFRxHttpClient.requestCostDownloadNegative,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'importSource', 'sha256' )
			end,
			relPathFn = function( _, docValue, _, fileSpec )
				return fileSpec:pathForProxy( docValue._id, '360p' )
			end
		},
		-- TODO [immisch 2022-03-22]: treating this as a proxy for now (i.e. something that is not regenerated when develop settings change)
		[ '/rels/rendition_type/720p' ] = {
			shortName = 'proxy_720p',
			localOnly = { '_localOnly', 'files', 'proxy', '720p' },
			downloadCost = WFRxHttpClient.requestCostDownloadNegative,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'importSource', 'sha256' )
			end,
			relPathFn = function( _, docValue, _, fileSpec )
				return fileSpec:pathForProxy( docValue._id, '720p' )
			end
		},
		[ '/rels/proxy_type/1080p' ] = {
			shortName = 'proxy_1080p',
			localOnly = { '_localOnly', 'files', 'proxy', '1080p' },
			downloadCost = WFRxHttpClient.requestCostDownloadNegative,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'importSource', 'sha256' )
			end,
			relPathFn = function( _, docValue, _, fileSpec )
				return fileSpec:pathForProxy( docValue._id, '1080p' )
			end
		},
		[ '/rels/proxy_type/1080p709' ] = {
			shortName = 'proxy_1080p',
			localOnly = { '_localOnly', 'files', 'proxy', '1080p709' },
			downloadCost = WFRxHttpClient.requestCostDownloadNegative,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'importSource', 'sha256' )
			end,
			relPathFn = function( _, docValue, _, fileSpec )
				return fileSpec:pathForProxy( docValue._id, '1080p709' )
			end
		},
		[ '/rels/master' ] = {
			shortName = 'original',
			-- caution: path is original/master, not original/main
			localOnly = { '_localOnly', 'files', 'original', 'master' },
			downloadCost = WFRxHttpClient.requestCostDownloadNegative,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'importSource', 'sha256' )
			end,
			relPathFn = function( _, docValue, _, fileSpec )
				local captureDate = derefOpt( docValue, 'captureDate' ) or '0000-00-00T00:00:00Z'
				return fileSpec:pathForOriginal( docValue._id, captureDate )
			end
		},
		[ '/rels/xmp/develop' ] = {
			shortName = 'xmp_develop',
			localOnly = { '_localOnly', 'files', 'xmp_develop' },
			downloadCost = WFRxHttpClient.requestCostDownloadXmp,
			downloadPriority = WFRxHttpClient.requestPriorityMedium,
			contentIdFn = function( _, docValue )
				return derefOpt( docValue, 'develop', 'xmpCameraRaw', 'sha256' )
			end,
			relPathFn = function( _, _, contentId, fileSpec )
				return fileSpec:pathForSettingsXmp( contentId )
			end,
		}
	}

end

--------------------------------------------------------------------------------

WFCatFileConductorConstants.TEST_getRelativeBlobPath = getRelativeBlobPath
WFCatFileConductorConstants.TEST_getCatalogBasePath = getCatalogBasePath

return WFCatFileConductorConstants