Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions GDJS/Runtime/ResourceLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ namespace gdjs {
);
};

const encodeLocalResourceUrl = (url: string): string => {
if (
url.startsWith('http://') ||
url.startsWith('https://') ||
url.startsWith('data:') ||
url.startsWith('blob:')
) {
return url;
}

return encodeURI(url).replace(/#/g, '%23');
};

/**
* A task of pre-loading resources used by a scene.
*
Expand Down Expand Up @@ -617,6 +630,8 @@ namespace gdjs {
* the resource (this can be for example a token needed to access the resource).
*/
getFullUrl(url: string) {
url = encodeLocalResourceUrl(url);

if (this._runtimeGame.isInGameEdition()) {
// Avoid adding cache burst to URLs which are assumed to be immutable files,
// to avoid costly useless requests each time the game is hot-reloaded.
Expand Down
129 changes: 82 additions & 47 deletions newIDE/app/scripts/import-libGD.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,28 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) {
return branch;
};

// Try to download libGD.js from a specific commit on the current branch
const downloadCommitLibGdJs = (branch, gitRef) =>
const getHashFromGitRef = gitRef => {
const hashShellString = shell.exec(`git rev-parse "${gitRef}"`, {
silent: true,
});
const hash = (hashShellString.stdout || 'unknown-hash').trim();

if (hashShellString.stderr || hashShellString.code) {
shell.echo(`⚠️ Can't find the hash of the associated commit.`);
return null;
}

return hash;
};

// Try to download libGD.js from a specific commit on the inferred branch.
const downloadCommitLibGdJs = gitRef =>
new Promise((resolve, reject) => {
shell.echo(`ℹ️ Trying to download libGD.js for ${gitRef}.`);

var hashShellString = shell.exec(`git rev-parse "${gitRef}"`, {
silent: true,
});
const hash = (hashShellString.stdout || 'unknown-hash').trim();
const hash = getHashFromGitRef(gitRef);
const branch = getBranchFromGitRef(gitRef);
if (hashShellString.stderr || hashShellString.code || !branch) {
if (!hash || !branch) {
shell.echo(
`⚠️ Can't find the hash or branch of the associated commit.`
);
Expand All @@ -91,6 +102,23 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) {
);
});

const downloadMasterCommitLibGdJs = gitRef =>
new Promise((resolve, reject) => {
shell.echo(`ℹ️ Trying to download libGD.js for ${gitRef} from master.`);

const hash = getHashFromGitRef(gitRef);
if (!hash) {
reject();
return;
}

resolve(
downloadLibGdJs(
`https://s3.amazonaws.com/gdevelop-gdevelop.js/master/commit/${hash}`
)
);
});

// Try to download libGD.js from the latest version built for master branch.
const downloadBranchLatestLibGdJs = branchName => {
shell.echo(
Expand Down Expand Up @@ -151,48 +179,55 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) {
};

const branch = getBranchFromGitRef('HEAD');
const downloadCommitLibGdJsWithMasterFallback = gitRef =>
downloadCommitLibGdJs(gitRef).catch(() =>
downloadMasterCommitLibGdJs(gitRef)
);
const tryDownloadInOrder = downloaders =>
downloaders.reduce(
(previousDownload, download) => previousDownload.catch(() => download()),
Promise.reject()
);

// Try to download the latest libGD.js, fallback to previous or master ones
// if not found (including different parents, for handling of merge commits).
downloadCommitLibGdJs(branch, 'HEAD').then(onLibGdJsDownloaded, () => {
// Force the exact version of GDevelop.js to be downloaded for AppVeyor - because
// this means we build the app and we don't want to risk mismatch (Core C++ not up to date
// with the IDE JavaScript).
if (process.env.APPVEYOR || process.env.REQUIRES_EXACT_LIBGD_JS_VERSION) {
shell.echo(
`❌ Can't download the exact required version of libGD.js - check it was built by CircleCI before running this CI.`
);
shell.echo(
`ℹ️ See the pipeline on https://app.circleci.com/pipelines/github/4ian/GDevelop.`
);
shell.exit(1);
}
downloadCommitLibGdJsWithMasterFallback('HEAD').then(
onLibGdJsDownloaded,
() => {
// Force the exact version of GDevelop.js to be downloaded for AppVeyor - because
// this means we build the app and we don't want to risk mismatch (Core C++ not up to date
// with the IDE JavaScript).
if (process.env.APPVEYOR || process.env.REQUIRES_EXACT_LIBGD_JS_VERSION) {
shell.echo(
`❌ Can't download the exact required version of libGD.js - check it was built by CircleCI before running this CI.`
);
shell.echo(
`ℹ️ See the pipeline on https://app.circleci.com/pipelines/github/4ian/GDevelop.`
);
shell.exit(1);
}

downloadCommitLibGdJs(branch, 'HEAD~1').then(onLibGdJsDownloaded, () =>
downloadCommitLibGdJs(branch, 'HEAD~2').then(onLibGdJsDownloaded, () =>
downloadCommitLibGdJs(branch, 'HEAD~3').then(onLibGdJsDownloaded, () =>
downloadBranchLatestLibGdJs(branch).then(onLibGdJsDownloaded, () =>
downloadBranchLatestLibGdJs('master').then(
onLibGdJsDownloaded,
() => {
if (alreadyHasLibGdJs) {
shell.echo(
`ℹ️ Can't download any version of libGD.js, assuming you can go ahead with the existing one.`
);
shell.exit(0);
return;
} else {
shell.echo(
`❌ Can't download any version of libGD.js, please check your internet connection.`
);
shell.exit(1);
return;
}
}
)
)
)
)
);
});
tryDownloadInOrder([
() => downloadCommitLibGdJsWithMasterFallback('HEAD~1'),
() => downloadCommitLibGdJsWithMasterFallback('HEAD~2'),
() => downloadCommitLibGdJsWithMasterFallback('HEAD~3'),
() => downloadBranchLatestLibGdJs(branch),
() => downloadBranchLatestLibGdJs('master'),
]).then(onLibGdJsDownloaded, () => {
if (alreadyHasLibGdJs) {
shell.echo(
`ℹ️ Can't download any version of libGD.js, assuming you can go ahead with the existing one.`
);
shell.exit(0);
return;
} else {
shell.echo(
`❌ Can't download any version of libGD.js, please check your internet connection.`
);
shell.exit(1);
return;
}
});
}
);
}
60 changes: 60 additions & 0 deletions newIDE/app/src/ResourcesList/ResourceUrlEncoding.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// @flow
import ResourcesLoader from '../ResourcesLoader';

jest.mock('../Utils/OptionalRequire');
jest.mock('../Utils/CrossOrigin', () => ({
addGDevelopResourceTokenIfRequired: url => url,
}));

describe('ResourceUrlEncoding', () => {
let dateNowSpy;

const makeProject = (ptr: number): any => ({
ptr,
getProjectFile: () => 'E:\\GDevelop\\Weekend Jam #1\\game.json',
getResourcesManager: () => ({
hasResource: () => true,
getResource: () => ({
getFile: () => 'Parts/hero #1.png',
}),
}),
});

beforeEach(() => {
ResourcesLoader.burstAllUrlsCache();
dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(1234);
});

afterEach(() => {
dateNowSpy.mockRestore();
});

it('encodes hashes in local resource urls', () => {
const project = makeProject(1);

const fullUrl = ResourcesLoader.getResourceFullUrl(
project,
'Parts/hero #1.png',
{ disableCacheBurst: true }
);

expect(fullUrl).toContain('%23');
expect(fullUrl).not.toContain('#');
expect(fullUrl).not.toContain('?cache=');
});

it('adds cache busting after the encoded local resource url', () => {
const project = makeProject(2);

const fullUrl = ResourcesLoader.getResourceFullUrl(
project,
'Parts/hero #1.png',
{}
);

expect(fullUrl).toContain('%23');
expect(fullUrl).not.toContain('#');
expect(fullUrl).toContain('?cache=1234');
expect(fullUrl.indexOf('%23')).toBeLessThan(fullUrl.indexOf('?cache=1234'));
});
});
12 changes: 10 additions & 2 deletions newIDE/app/src/ResourcesList/ResourceUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import newNameGenerator from '../Utils/NewNameGenerator';
import { toNewGdMapStringString } from '../Utils/MapStringString';
const fs = optionalRequire('fs');
const path = optionalRequire('path');
const url = optionalRequire('url');
const gd: libGDevelop = global.gd;

export const createOrUpdateResource = (
Expand Down Expand Up @@ -39,7 +40,7 @@ export const getLocalResourceFullPath = (
project,
resourceName,
{}
).substring(7 /* Remove "file://" from the URL to get a local path */);
);

if (resourcePath.indexOf('?cache=') !== -1) {
// Remove, if needed, the cache bursting argument from the URL.
Expand All @@ -48,7 +49,14 @@ export const getLocalResourceFullPath = (
resourcePath.lastIndexOf('?cache=')
);
}
return resourcePath;

if (resourcePath.startsWith('file://') && url && url.fileURLToPath) {
return url.fileURLToPath(resourcePath);
}

return resourcePath.substring(
7 /* Remove "file://" from the URL to get a local path */
);
};

export const isPathInProjectFolder = (
Expand Down
24 changes: 24 additions & 0 deletions newIDE/app/src/ResourcesList/ResourceUtils.spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
// @flow
import {
getLocalResourceFullPath,
parseLocalFilePathOrExtensionFromMetadata,
renameResourcesInProject,
updateResourceJsonMetadata,
} from './ResourceUtils';
jest.mock('../ResourcesLoader', () => ({
__esModule: true,
default: {
getResourceFullUrl: jest.fn(),
},
}));
const ResourcesLoader = require('../ResourcesLoader').default;
const gd: libGDevelop = global.gd;

const addNewAnimationWithImageToSpriteObject = (
Expand All @@ -22,6 +30,22 @@ const addNewAnimationWithImageToSpriteObject = (
};

describe('ResourceUtils', () => {
beforeEach(() => {
ResourcesLoader.getResourceFullUrl.mockReset();
});

it('can decode an encoded local file URL', () => {
const project: any = {};
ResourcesLoader.getResourceFullUrl.mockReturnValue(
'file:///E:/GDevelop/Weekend%20Jam%20%231/Parts/hero%20%231.png?cache=1234'
);

expect([
'E:\\GDevelop\\Weekend Jam #1\\Parts\\hero #1.png',
'/E:/GDevelop/Weekend Jam #1/Parts/hero #1.png',
]).toContain(getLocalResourceFullPath(project, 'hero'));
});

it('can rename a resource in the whole project', () => {
const project = gd.ProjectHelper.createNewGDJSProject();

Expand Down
11 changes: 7 additions & 4 deletions newIDE/app/src/ResourcesLoader/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { addGDevelopResourceTokenIfRequired } from '../Utils/CrossOrigin';
import optionalRequire from '../Utils/OptionalRequire';
const electron = optionalRequire('electron');
const path = optionalRequire('path');
const url = optionalRequire('url');

class UrlsCache {
projectCache: { [number]: { [string]: string } } = {};
Expand Down Expand Up @@ -132,15 +133,17 @@ export default class ResourcesLoader {
// Support local filesystem with Electron
const file = project.getProjectFile();
const projectPath = path.dirname(file);
const resourceAbsolutePath = path
.resolve(projectPath, urlOrFilename)
.replace(/\\/g, '/');
const resourceAbsolutePath = path.resolve(projectPath, urlOrFilename);
const resourceFileUrl =
url && url.pathToFileURL
? url.pathToFileURL(resourceAbsolutePath).toString()
: 'file://' + resourceAbsolutePath.replace(/\\/g, '/');

console.info('Caching resolved local filename:', resourceAbsolutePath);
return this._cache.cacheLocalFileUrl(
project,
urlOrFilename,
'file://' + resourceAbsolutePath,
resourceFileUrl,
!!disableCacheBurst
);
}
Expand Down
3 changes: 3 additions & 0 deletions newIDE/app/src/Utils/__mocks__/OptionalRequire.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const mockOptionalRequire = jest.fn(
if (moduleName === 'path') {
return path;
}
if (moduleName === 'url') {
return require('url');
}
if (moduleName === 'os') {
return mockOs;
}
Expand Down