Skip to content

Commit

Permalink
appservice: Fix site files by avoiding kudu redirect (#1537)
Browse files Browse the repository at this point in the history
* Use href property instead of calculating path for site files

* Bump version

* Rename href to url

* Remove api version
  • Loading branch information
alexweininger authored Jul 20, 2023
1 parent 27602ff commit c274510
Show file tree
Hide file tree
Showing 8 changed files with 38 additions and 41 deletions.
4 changes: 2 additions & 2 deletions appservice/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion appservice/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@microsoft/vscode-azext-azureappservice",
"author": "Microsoft Corporation",
"version": "2.1.2",
"version": "2.2.0",
"description": "Common tools for developing Azure App Service extensions for VS Code",
"tags": [
"azure",
Expand Down
8 changes: 4 additions & 4 deletions appservice/src/SiteClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,20 +433,20 @@ export class SiteClient implements IAppSettingsClient {
}) as KuduModels.LogEntry[];
}

public async vfsGetItem(context: IActionContext, path: string): Promise<AzExtPipelineResponse> {
public async vfsGetItem(context: IActionContext, url: string): Promise<AzExtPipelineResponse> {
const client: ServiceClient = await createGenericClient(context, this._site.subscription);
return await client.sendRequest(createPipelineRequest({
method: 'GET',
url: `${this._site.kuduUrl}/api/vfs/${path}?api-version=2022-03-01`,
url,
}));
}

public async vfsPutItem(context: IActionContext, data: string | ArrayBuffer, path: string, rawHeaders?: {}): Promise<AzExtPipelineResponse> {
public async vfsPutItem(context: IActionContext, data: string | ArrayBuffer, url: string, rawHeaders?: {}): Promise<AzExtPipelineResponse> {
const client: ServiceClient = await createGenericClient(context, this._site.subscription);
const headers = createHttpHeaders(rawHeaders);
return await client.sendRequest(createPipelineRequest({
method: 'PUT',
url: `${this._site.kuduUrl}/api/vfs/${path}?api-version=2022-03-01`,
url,
body: typeof data === 'string' ? data : data.toString(),
headers
}));
Expand Down
32 changes: 17 additions & 15 deletions appservice/src/siteFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { RestError, createPipelineRequest } from '@azure/core-rest-pipeline';
import { AzExtPipelineResponse, createGenericClient } from '@microsoft/vscode-azext-azureutils';
import { IActionContext, IParsedError, parseError } from '@microsoft/vscode-azext-utils';
import * as retry from 'p-retry';
import * as path from 'path';
import { ParsedSite } from './SiteClient';

export interface ISiteFile {
Expand All @@ -20,12 +19,20 @@ export interface ISiteFileMetadata {
mime: string;
name: string;
path: string;
href: string;
}

export async function getFile(context: IActionContext, site: ParsedSite, filePath: string): Promise<ISiteFile> {
/**
* @param path - Do not include leading slash. Include trailing slash if path represents a folder.
*/
export function createSiteFilesUrl(site: ParsedSite, path: string): string {
return `${site.kuduUrl}/api/vfs/${path}`
}

export async function getFile(context: IActionContext, site: ParsedSite, url: string): Promise<ISiteFile> {
let response: AzExtPipelineResponse;
try {
response = await getFsResponse(context, site, filePath);
response = await getFsResponse(context, site, url);
} catch (error) {
if (error instanceof RestError && error.code === 'PARSE_ERROR' && error.response?.status === 200) {
// Some files incorrectly list the content-type as json and fail to parse, but we always just want the text itself
Expand All @@ -37,8 +44,8 @@ export async function getFile(context: IActionContext, site: ParsedSite, filePat
return { data: <string>response.bodyAsText, etag: <string>response.headers.get('etag') };
}

export async function listFiles(context: IActionContext, site: ParsedSite, filePath: string): Promise<ISiteFileMetadata[]> {
const response: AzExtPipelineResponse = await getFsResponse(context, site, filePath);
export async function listFiles(context: IActionContext, site: ParsedSite, url: string): Promise<ISiteFileMetadata[]> {
const response: AzExtPipelineResponse = await getFsResponse(context, site, url);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Array.isArray(response.parsedBody) ? response.parsedBody : [];
}
Expand All @@ -47,24 +54,19 @@ export async function listFiles(context: IActionContext, site: ParsedSite, fileP
* Overwrites or creates a file. The etag passed in may be `undefined` if the file is being created
* Returns the latest etag of the updated file
*/
export async function putFile(context: IActionContext, site: ParsedSite, data: string | ArrayBuffer, filePath: string, etag: string | undefined): Promise<string> {
export async function putFile(context: IActionContext, site: ParsedSite, data: string | ArrayBuffer, url: string, etag: string | undefined): Promise<string> {
const options: {} = etag ? { ['If-Match']: etag } : {};
const kuduClient = await site.createClient(context);
const result: AzExtPipelineResponse = (await kuduClient.vfsPutItem(context, data, filePath, options));
const result: AzExtPipelineResponse = (await kuduClient.vfsPutItem(context, data, url, options));
return <string>result.headers.get('etag');
}

/**
* Kudu APIs don't work for Linux consumption function apps and ARM APIs don't seem to work for web apps. We'll just have to use both
*/
async function getFsResponse(context: IActionContext, site: ParsedSite, filePath: string): Promise<AzExtPipelineResponse> {
async function getFsResponse(context: IActionContext, site: ParsedSite, url: string): Promise<AzExtPipelineResponse> {
try {
if (site.isFunctionApp) {
const linuxHome: string = '/home';
if (site.isLinux && !filePath.startsWith(linuxHome)) {
filePath = path.posix.join(linuxHome, filePath);
}

/*
Related to issue: https://github.com/microsoft/vscode-azurefunctions/issues/3337
Sometimes receive a 'BadGateway' or 'ServiceUnavailable' error on initial fetch, but consecutive re-fetching usually fixes the issue.
Expand All @@ -80,7 +82,7 @@ async function getFsResponse(context: IActionContext, site: ParsedSite, filePath
try {
return await client.sendRequest(createPipelineRequest({
method: 'GET',
url: `${site.id}/hostruntime/admin/vfs/${filePath}/?api-version=2022-03-01`
url,
}));
} catch (error) {
const parsedError: IParsedError = parseError(error);
Expand All @@ -95,7 +97,7 @@ async function getFsResponse(context: IActionContext, site: ParsedSite, filePath
);
} else {
const kuduClient = await site.createClient(context);
return await kuduClient.vfsGetItem(context, filePath);
return await kuduClient.vfsGetItem(context, url);
}
} catch (error) {
context.telemetry.maskEntireErrorMessage = true; // since the error could have the contents of the user's file
Expand Down
8 changes: 4 additions & 4 deletions appservice/src/tree/FileTreeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ import { FolderTreeItem } from './FolderTreeItem';
export class FileTreeItem extends AzExtTreeItem {
public static contextValue: string = 'file';
public readonly label: string;
public readonly path: string;
public readonly url: string;
public readonly isReadOnly: boolean;
public readonly site: ParsedSite;
public readonly parent: FolderTreeItem;

constructor(parent: FolderTreeItem, site: ParsedSite, label: string, path: string, isReadOnly: boolean) {
constructor(parent: FolderTreeItem, site: ParsedSite, label: string, url: string, isReadOnly: boolean) {
super(parent);
this.site = site;
this.label = label;
this.path = path;
this.url = url;
this.isReadOnly = isReadOnly;
}

Expand All @@ -43,7 +43,7 @@ export class FileTreeItem extends AzExtTreeItem {

public async openReadOnly(context: IActionContext): Promise<void> {
await this.runWithTemporaryDescription(context, l10n.t('Opening...'), async () => {
const file: ISiteFile = await getFile(context, this.site, this.path);
const file: ISiteFile = await getFile(context, this.site, this.url);
await openReadOnlyContent(this, file.data, '');
});
}
Expand Down
19 changes: 6 additions & 13 deletions appservice/src/tree/FolderTreeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { FileTreeItem } from './FileTreeItem';
export interface FolderTreeItemOptions {
site: ParsedSite;
label: string;
path: string;
url: string;
isReadOnly: boolean;
contextValuesToAdd?: string[];
}
Expand All @@ -21,7 +21,7 @@ export class FolderTreeItem extends AzExtParentTreeItem {
public static contextValue: string = 'folder';
public readonly childTypeLabel: string = l10n.t('file or folder');
public readonly label: string;
public readonly path: string;
public readonly url: string;
public readonly isReadOnly: boolean;

public readonly contextValuesToAdd: string[];
Expand All @@ -33,7 +33,7 @@ export class FolderTreeItem extends AzExtParentTreeItem {
super(parent);
this.site = options.site;
this.label = options.label;
this.path = options.path;
this.url = options.url;
this.isReadOnly = options.isReadOnly;
this.contextValuesToAdd = options.contextValuesToAdd || [];
}
Expand All @@ -55,24 +55,17 @@ export class FolderTreeItem extends AzExtParentTreeItem {
}

public async loadMoreChildrenImpl(_clearCache: boolean, context: IActionContext): Promise<AzExtTreeItem[]> {
let files: ISiteFileMetadata[] = await listFiles(context, this.site, this.path);

let files: ISiteFileMetadata[] = await listFiles(context, this.site, this.url);
// this file is being accessed by Kudu and is not viewable
files = files.filter(f => f.mime !== 'text/xml' || !f.name.includes('LogFiles-kudu-trace_pending.xml'));

return files.map(file => {
const home: string = 'home';
// truncate the home of the path
// the substring starts at file.path.indexOf(home) because the path sometimes includes site/ or D:\
// the home.length + 1 is to account for the trailing slash, Linux uses / and Window uses \
const fsPath: string = file.path.substring(file.path.indexOf(home) + home.length + 1);
return file.mime === 'inode/directory' ? new FolderTreeItem(this, {
site: this.site,
label: file.name,
isReadOnly: this.isReadOnly,
path: fsPath,
url: file.href,
contextValuesToAdd: this.contextValuesToAdd
}) : new FileTreeItem(this, this.site, file.name, fsPath, this.isReadOnly);
}) : new FileTreeItem(this, this.site, file.name, file.href, this.isReadOnly);
});
}

Expand Down
3 changes: 2 additions & 1 deletion appservice/src/tree/LogFilesTreeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { l10n, ThemeIcon } from 'vscode';
import { ext } from '../extensionVariables';
import { ParsedSite } from '../SiteClient';
import { FolderTreeItem } from './FolderTreeItem';
import { createSiteFilesUrl } from '../siteFiles';

interface LogFilesTreeItemOptions {
site: ParsedSite;
Expand All @@ -28,7 +29,7 @@ export class LogFilesTreeItem extends FolderTreeItem {
super(parent, {
site: options.site,
label: l10n.t('Logs'),
path: '/LogFiles',
url: createSiteFilesUrl(options.site, 'LogFiles/'),
isReadOnly: true,
contextValuesToAdd: options.contextValuesToAdd || []
});
Expand Down
3 changes: 2 additions & 1 deletion appservice/src/tree/SiteFilesTreeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AzExtParentTreeItem, createContextValue } from '@microsoft/vscode-azext
import * as vscode from 'vscode';
import { ParsedSite } from '../SiteClient';
import { FolderTreeItem } from './FolderTreeItem';
import { createSiteFilesUrl } from '../siteFiles';

interface SiteFilesTreeItemOptions {
site: ParsedSite;
Expand All @@ -26,7 +27,7 @@ export class SiteFilesTreeItem extends FolderTreeItem {
super(parent, {
site: options.site,
label: vscode.l10n.t('Files'),
path: '/site/wwwroot',
url: createSiteFilesUrl(options.site, 'site/wwwroot/'),
isReadOnly: options.isReadOnly
});
this.contextValuesToAdd = options.contextValuesToAdd || [];
Expand Down

0 comments on commit c274510

Please sign in to comment.