Merge pull request #50645 from nextcloud/fix/refresh-convert-list

This commit is contained in:
John Molakvoæ 2025-02-04 17:45:49 +01:00 committed by GitHub
commit a2e05eeca3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 108 additions and 126 deletions

View file

@ -11,7 +11,7 @@ import { t } from '@nextcloud/l10n'
import AutoRenewSvg from '@mdi/svg/svg/autorenew.svg?raw'
import { convertFile, convertFiles, getParentFolder } from './convertUtils'
import { convertFile, convertFiles } from './convertUtils'
type ConversionsProvider = {
from: string,
@ -33,17 +33,17 @@ export const registerConvertActions = () => {
return nodes.every(node => from === node.mime)
},
async exec(node: Node, view: View, dir: string) {
async exec(node: Node) {
// If we're here, we know that the node has a fileid
convertFile(node.fileid as number, to, getParentFolder(view, dir))
convertFile(node.fileid as number, to)
// Silently terminate, we'll handle the UI in the background
return null
},
async execBatch(nodes: Node[], view: View, dir: string) {
async execBatch(nodes: Node[]) {
const fileIds = nodes.map(node => node.fileid).filter(Boolean) as number[]
convertFiles(fileIds, to, getParentFolder(view, dir))
convertFiles(fileIds, to)
// Silently terminate, we'll handle the UI in the background
return Array(nodes.length).fill(null)

View file

@ -2,23 +2,34 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { AxiosResponse } from '@nextcloud/axios'
import type { Folder, View } from '@nextcloud/files'
import type { AxiosResponse, AxiosError } from '@nextcloud/axios'
import type { OCSResponse } from '@nextcloud/typings/ocs'
import { emit } from '@nextcloud/event-bus'
import { generateOcsUrl } from '@nextcloud/router'
import { showError, showLoading, showSuccess } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import axios, { isAxiosError } from '@nextcloud/axios'
import PQueue from 'p-queue'
import { fetchNode } from '../services/WebdavClient.ts'
import logger from '../logger'
import { useFilesStore } from '../store/files'
import { getPinia } from '../store'
import { usePathsStore } from '../store/paths'
type ConversionResponse = {
path: string
fileId: number
}
interface PromiseRejectedResult<T> {
status: 'rejected'
reason: T
}
type PromiseSettledResult<T, E> = PromiseFulfilledResult<T> | PromiseRejectedResult<E>;
type ConversionSuccess = AxiosResponse<OCSResponse<ConversionResponse>>
type ConversionError = AxiosError<OCSResponse<ConversionResponse>>
const queue = new PQueue({ concurrency: 5 })
const requestConversion = function(fileId: number, targetMimeType: string): Promise<AxiosResponse> {
return axios.post(generateOcsUrl('/apps/files/api/v1/convert'), {
fileId,
@ -26,7 +37,7 @@ const requestConversion = function(fileId: number, targetMimeType: string): Prom
})
}
export const convertFiles = async function(fileIds: number[], targetMimeType: string, parentFolder: Folder | null) {
export const convertFiles = async function(fileIds: number[], targetMimeType: string) {
const conversions = fileIds.map(fileId => queue.add(() => requestConversion(fileId, targetMimeType)))
// Start conversion
@ -34,14 +45,14 @@ export const convertFiles = async function(fileIds: number[], targetMimeType: st
// Handle results
try {
const results = await Promise.allSettled(conversions)
const failed = results.filter(result => result.status === 'rejected')
const results = await Promise.allSettled(conversions) as PromiseSettledResult<ConversionSuccess, ConversionError>[]
const failed = results.filter(result => result.status === 'rejected') as PromiseRejectedResult<ConversionError>[]
if (failed.length > 0) {
const messages = failed.map(result => result.reason?.response?.data?.ocs?.meta?.message) as string[]
const messages = failed.map(result => result.reason?.response?.data?.ocs?.meta?.message)
logger.error('Failed to convert files', { fileIds, targetMimeType, messages })
// If all failed files have the same error message, show it
if (new Set(messages).size === 1) {
if (new Set(messages).size === 1 && typeof messages[0] === 'string') {
showError(t('files', 'Failed to convert files: {message}', { message: messages[0] }))
return
}
@ -74,15 +85,27 @@ export const convertFiles = async function(fileIds: number[], targetMimeType: st
// All files converted
showSuccess(t('files', 'Files successfully converted'))
// Trigger a reload of the file list
if (parentFolder) {
emit('files:node:updated', parentFolder)
}
// Extract files that are within the current directory
// in batch mode, you might have files from different directories
// ⚠️, let's get the actual current dir, as the one from the action
// might have changed as the user navigated away
const currentDir = window.OCP.Files.Router.query.dir as string
const newPaths = results
.filter(result => result.status === 'fulfilled')
.map(result => result.value.data.ocs.data.path)
.filter(path => path.startsWith(currentDir))
// Fetch the new files
logger.debug('Files to fetch', { newPaths })
const newFiles = await Promise.all(newPaths.map(path => fetchNode(path)))
// Inform the file list about the new files
newFiles.forEach(file => emit('files:node:created', file))
// Switch to the new files
const firstSuccess = results[0] as PromiseFulfilledResult<AxiosResponse>
const firstSuccess = results[0] as PromiseFulfilledResult<ConversionSuccess>
const newFileId = firstSuccess.value.data.ocs.data.fileId
window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId }, window.OCP.Files.Router.query)
window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId.toString() }, window.OCP.Files.Router.query)
} catch (error) {
// Should not happen as we use allSettled and handle errors above
showError(t('files', 'Failed to convert files'))
@ -93,24 +116,23 @@ export const convertFiles = async function(fileIds: number[], targetMimeType: st
}
}
export const convertFile = async function(fileId: number, targetMimeType: string, parentFolder: Folder | null) {
export const convertFile = async function(fileId: number, targetMimeType: string) {
const toast = showLoading(t('files', 'Converting file…'))
try {
const result = await queue.add(() => requestConversion(fileId, targetMimeType)) as AxiosResponse
const result = await queue.add(() => requestConversion(fileId, targetMimeType)) as AxiosResponse<OCSResponse<ConversionResponse>>
showSuccess(t('files', 'File successfully converted'))
// Trigger a reload of the file list
if (parentFolder) {
emit('files:node:updated', parentFolder)
}
// Inform the file list about the new file
const newFile = await fetchNode(result.data.ocs.data.path)
emit('files:node:created', newFile)
// Switch to the new file
const newFileId = result.data.ocs.data.fileId
window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId }, window.OCP.Files.Router.query)
window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId.toString() }, window.OCP.Files.Router.query)
} catch (error) {
// If the server returned an error message, show it
if (error.response?.data?.ocs?.meta?.message) {
if (isAxiosError(error) && error.response?.data?.ocs?.meta?.message) {
showError(t('files', 'Failed to convert file: {message}', { message: error.response.data.ocs.meta.message }))
return
}
@ -122,26 +144,3 @@ export const convertFile = async function(fileId: number, targetMimeType: string
toast.hideToast()
}
}
/**
* Get the parent folder of a path
*
* TODO: replace by the parent node straight away when we
* update the Files actions api accordingly.
*
* @param view The current view
* @param path The path to the file
* @returns The parent folder
*/
export const getParentFolder = function(view: View, path: string): Folder | null {
const filesStore = useFilesStore(getPinia())
const pathsStore = usePathsStore(getPinia())
const parentSource = pathsStore.getPath(view.id, path)
if (!parentSource) {
return null
}
const parentFolder = filesStore.getNode(parentSource) as Folder | undefined
return parentFolder ?? null
}

View file

@ -2,17 +2,18 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
import type { FileStat, ResponseDataDetailed } from 'webdav'
import type { Node } from '@nextcloud/files'
export const client = davGetClient()
import { getClient, getDefaultPropfind, getRootPath, resultToNode } from '@nextcloud/files/dav'
export const fetchNode = async (node: Node): Promise<Node> => {
const propfindPayload = davGetDefaultPropfind()
const result = await client.stat(`${davRootPath}${node.path}`, {
export const client = getClient()
export const fetchNode = async (path: string): Promise<Node> => {
const propfindPayload = getDefaultPropfind()
const result = await client.stat(`${getRootPath()}${path}`, {
details: true,
data: propfindPayload,
}) as ResponseDataDetailed<FileStat>
return davResultToNode(result.data)
return resultToNode(result.data)
}

View file

@ -135,7 +135,7 @@ export const useFilesStore = function(...args) {
// If we have multiple nodes with the same file ID, we need to update all of them
const nodes = this.getNodesById(node.fileid)
if (nodes.length > 1) {
await Promise.all(nodes.map(fetchNode)).then(this.updateNodes)
await Promise.all(nodes.map(node => fetchNode(node.path))).then(this.updateNodes)
logger.debug(nodes.length + ' nodes updated in store', { fileid: node.fileid })
return
}
@ -147,7 +147,7 @@ export const useFilesStore = function(...args) {
}
// Otherwise, it means we receive an event for a node that is not in the store
fetchNode(node).then(n => this.updateNodes([n]))
fetchNode(node.path).then(n => this.updateNodes([n]))
},
// Handlers for legacy sidebar (no real nodes support)

View file

@ -491,7 +491,7 @@ export default {
this.loading = true
try {
this.node = await fetchNode({ path: this.file })
this.node = await fetchNode(this.file)
this.fileInfo = FileInfo(this.node)
// adding this as fallback because other apps expect it
this.fileInfo.dir = this.file.split('/').slice(0, -1).join('/')

View file

@ -7,7 +7,6 @@ import { getCurrentUser } from '@nextcloud/auth'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { ShareType } from '@nextcloud/sharing'
import { emit } from '@nextcloud/event-bus'
import { fetchNode } from '../services/WebdavClient.ts'
import PQueue from 'p-queue'
import debounce from 'debounce'
@ -20,6 +19,7 @@ import logger from '../services/logger.ts'
import {
BUNDLED_PERMISSIONS,
} from '../lib/SharePermissionsToolBox.js'
import { fetchNode } from '../../../files/src/services/WebdavClient.ts'
export default {
mixins: [SharesRequests],
@ -164,7 +164,7 @@ export default {
async getNode() {
const node = { path: this.path }
try {
this.node = await fetchNode(node)
this.node = await fetchNode(node.path)
logger.info('Fetched node:', { node: this.node })
} catch (error) {
logger.error('Error:', error)

View file

@ -1,18 +0,0 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
import type { FileStat, ResponseDataDetailed } from 'webdav'
import type { Node } from '@nextcloud/files'
export const client = davGetClient()
export const fetchNode = async (node: Node): Promise<Node> => {
const propfindPayload = davGetDefaultPropfind()
const result = await client.stat(`${davRootPath}${node.path}`, {
details: true,
data: propfindPayload,
}) as ResponseDataDetailed<FileStat>
return davResultToNode(result.data)
}

2
dist/4590-4590.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
4590-4590.js.license

2
dist/512-512.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/512-512.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/512-512.js.map.license vendored Symbolic link
View file

@ -0,0 +1 @@
512-512.js.license

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/files-init.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
(()=>{"use strict";var e,t,r,o={3360:(e,t,r)=>{var o=r(85168),n=r(32981),a=r(53334);window.addEventListener("DOMContentLoaded",(function(){const{updateLink:e,updateVersion:t}=(0,n.C)("updatenotification","updateState"),r=(0,a.t)("core","{version} is available. Get more information on how to update.",{version:t});(0,o.cf)(r,{onClick:()=>window.open(e,"_blank")})}))}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(r.exports,r,r.exports,a),r.loaded=!0,r.exports}a.m=o,e=[],a.O=(t,r,o,n)=>{if(!r){var i=1/0;for(u=0;u<e.length;u++){r=e[u][0],o=e[u][1],n=e[u][2];for(var l=!0,c=0;c<r.length;c++)(!1&n||i>=n)&&Object.keys(a.O).every((e=>a.O[e](r[c])))?r.splice(c--,1):(l=!1,n<i&&(i=n));if(l){e.splice(u--,1);var d=o();void 0!==d&&(t=d)}}return t}n=n||0;for(var u=e.length;u>0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[r,o,n]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((t,r)=>(a.f[r](e,t),t)),[])),a.u=e=>e+"-"+e+".js?v="+{2441:"fc741cf57e9647f370a3",5862:"7b9b02dc0a1b898066ef",7874:"5d0f14697282cbdd7841"}[e],a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),t={},r="nextcloud:",a.l=(e,o,n,i)=>{if(t[e])t[e].push(o);else{var l,c;if(void 0!==n)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var s=d[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==r+n){l=s;break}}l||(c=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",r+n),l.src=e),t[e]=[o];var p=(r,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=t[e];if(delete t[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.forEach((e=>e(o))),r)return r(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),c&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=5169,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var t=a.g.document;if(!e&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var o=r.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=r[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={5169:0};a.f.j=(t,r)=>{var o=a.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise(((r,n)=>o=e[t]=[r,n]));r.push(o[2]=n);var i=a.p+a.u(t),l=new Error;a.l(i,(r=>{if(a.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;l.message="Loading chunk "+t+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}}),"chunk-"+t,t)}},a.O.j=t=>0===e[t];var t=(t,r)=>{var o,n,i=r[0],l=r[1],c=r[2],d=0;if(i.some((t=>0!==e[t]))){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(c)var u=c(a)}for(t&&t(r);d<i.length;d++)n=i[d],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(u)},r=self.webpackChunknextcloud=self.webpackChunknextcloud||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})(),a.nc=void 0;var i=a.O(void 0,[4208],(()=>a(3360)));i=a.O(i)})();
//# sourceMappingURL=updatenotification-update-notification-legacy.js.map?v=4f9797ff0d40b3bfc879
(()=>{"use strict";var e,t,r,o={3360:(e,t,r)=>{var o=r(85168),n=r(32981),a=r(53334);window.addEventListener("DOMContentLoaded",(function(){const{updateLink:e,updateVersion:t}=(0,n.C)("updatenotification","updateState"),r=(0,a.t)("core","{version} is available. Get more information on how to update.",{version:t});(0,o.cf)(r,{onClick:()=>window.open(e,"_blank")})}))}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(r.exports,r,r.exports,a),r.loaded=!0,r.exports}a.m=o,e=[],a.O=(t,r,o,n)=>{if(!r){var i=1/0;for(u=0;u<e.length;u++){r=e[u][0],o=e[u][1],n=e[u][2];for(var l=!0,d=0;d<r.length;d++)(!1&n||i>=n)&&Object.keys(a.O).every((e=>a.O[e](r[d])))?r.splice(d--,1):(l=!1,n<i&&(i=n));if(l){e.splice(u--,1);var c=o();void 0!==c&&(t=c)}}return t}n=n||0;for(var u=e.length;u>0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[r,o,n]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((t,r)=>(a.f[r](e,t),t)),[])),a.u=e=>e+"-"+e+".js?v="+{2441:"fc741cf57e9647f370a3",5862:"7b9b02dc0a1b898066ef",7874:"5d0f14697282cbdd7841"}[e],a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),t={},r="nextcloud:",a.l=(e,o,n,i)=>{if(t[e])t[e].push(o);else{var l,d;if(void 0!==n)for(var c=document.getElementsByTagName("script"),u=0;u<c.length;u++){var s=c[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==r+n){l=s;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",r+n),l.src=e),t[e]=[o];var p=(r,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=t[e];if(delete t[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.forEach((e=>e(o))),r)return r(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),d&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=5169,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var t=a.g.document;if(!e&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var o=r.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=r[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={5169:0};a.f.j=(t,r)=>{var o=a.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise(((r,n)=>o=e[t]=[r,n]));r.push(o[2]=n);var i=a.p+a.u(t),l=new Error;a.l(i,(r=>{if(a.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;l.message="Loading chunk "+t+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}}),"chunk-"+t,t)}},a.O.j=t=>0===e[t];var t=(t,r)=>{var o,n,i=r[0],l=r[1],d=r[2],c=0;if(i.some((t=>0!==e[t]))){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(d)var u=d(a)}for(t&&t(r);c<i.length;c++)n=i[c],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(u)},r=self.webpackChunknextcloud=self.webpackChunknextcloud||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})(),a.nc=void 0;var i=a.O(void 0,[4208],(()=>a(3360)));i=a.O(i)})();
//# sourceMappingURL=updatenotification-update-notification-legacy.js.map?v=03f0fa608e57f1778639

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long