提交 43184b2b 编写于 作者: J Johannes Rieken

add storageUri and globalStorageUri to ExtensionContext,...

add storageUri and globalStorageUri to ExtensionContext, https://github.com/microsoft/vscode/issues/101857
上级 a8a4e376
......@@ -2082,9 +2082,23 @@ declare module 'vscode' {
readonly logUri: Uri;
/**
* Short-hand for [`logUri.fsPath`](#ExtensionContext#logUri)
* The uri of a workspace specific directory in which the extension
* can store private state. The directory might not exist on disk and creation is
* up to the extension. However, the parent directory is guaranteed to be existent.
*
* Use [`workspaceState`](#ExtensionContext.workspaceState) or
* [`globalState`](#ExtensionContext.globalState) to store key value data.
*/
readonly storageUri: Uri | undefined;
/**
* The uri of a directory in which the extension can store global state.
* The directory might not exist on disk and creation is
* up to the extension. However, the parent directory is guaranteed to be existent.
*
* Use [`globalState`](#ExtensionContext.globalState) to store key value data.
*/
readonly logPath: string;
readonly globalStorageUri: Uri;
}
//#endregion
......
......@@ -75,6 +75,7 @@ import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentica
import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline';
import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument';
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
export interface IExtensionApiFactory {
(extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode;
......@@ -87,6 +88,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
// services
const initData = accessor.get(IExtHostInitDataService);
const extHostConsumerFileSystem = accessor.get(IExtHostConsumerFileSystem);
const extensionService = accessor.get(IExtHostExtensionService);
const extHostWorkspace = accessor.get(IExtHostWorkspace);
const extHostConfiguration = accessor.get(IExtHostConfiguration);
......@@ -746,7 +748,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
return extHostFileSystem.registerFileSystemProvider(scheme, provider, options);
},
get fs() {
return extHostFileSystem.fileSystem;
return extHostConsumerFileSystem;
},
registerFileSearchProvider: (scheme: string, provider: vscode.FileSearchProvider) => {
checkProposedApiEnabled(extension);
......
......@@ -386,14 +386,22 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
subscriptions: [],
get extensionUri() { return extensionDescription.extensionLocation; },
get extensionPath() { return extensionDescription.extensionLocation.fsPath; },
get storagePath() { return that._storagePath.workspaceValue(extensionDescription); },
get globalStoragePath() { return that._storagePath.globalValue(extensionDescription); },
asAbsolutePath(relativePath: string) { return path.join(extensionDescription.extensionLocation.fsPath, relativePath); },
get storagePath() { return that._storagePath.workspaceValue(extensionDescription)?.fsPath; },
get globalStoragePath() { return that._storagePath.globalValue(extensionDescription).fsPath; },
get logPath() { return path.join(that._initData.logsLocation.fsPath, extensionDescription.identifier.value); },
get logUri() {
checkProposedApiEnabled(extensionDescription);
return URI.joinPath(that._initData.logsLocation, extensionDescription.identifier.value);
},
get logPath() { return path.join(that._initData.logsLocation.fsPath, extensionDescription.identifier.value); },
get storageUri() {
checkProposedApiEnabled(extensionDescription);
return that._storagePath.workspaceValue(extensionDescription);
},
get globalStorageUri() {
checkProposedApiEnabled(extensionDescription);
return that._storagePath.globalValue(extensionDescription);
},
get extensionMode() { return extensionMode; },
get environmentVariableCollection() { return that._extHostTerminalService.getEnvironmentVariableCollection(extensionDescription); }
});
......
......@@ -8,7 +8,7 @@ import { MainContext, IMainContext, ExtHostFileSystemShape, MainThreadFileSystem
import type * as vscode from 'vscode';
import * as files from 'vs/platform/files/common/files';
import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
import { FileChangeType, FileSystemError } from 'vs/workbench/api/common/extHostTypes';
import { FileChangeType } from 'vs/workbench/api/common/extHostTypes';
import * as typeConverter from 'vs/workbench/api/common/extHostTypeConverters';
import { ExtHostLanguageFeatures } from 'vs/workbench/api/common/extHostLanguageFeatures';
import { Schemas } from 'vs/base/common/network';
......@@ -108,59 +108,6 @@ class FsLinkProvider {
}
}
class ConsumerFileSystem implements vscode.FileSystem {
constructor(private _proxy: MainThreadFileSystemShape) { }
stat(uri: vscode.Uri): Promise<vscode.FileStat> {
return this._proxy.$stat(uri).catch(ConsumerFileSystem._handleError);
}
readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
return this._proxy.$readdir(uri).catch(ConsumerFileSystem._handleError);
}
createDirectory(uri: vscode.Uri): Promise<void> {
return this._proxy.$mkdir(uri).catch(ConsumerFileSystem._handleError);
}
async readFile(uri: vscode.Uri): Promise<Uint8Array> {
return this._proxy.$readFile(uri).then(buff => buff.buffer).catch(ConsumerFileSystem._handleError);
}
writeFile(uri: vscode.Uri, content: Uint8Array): Promise<void> {
return this._proxy.$writeFile(uri, VSBuffer.wrap(content)).catch(ConsumerFileSystem._handleError);
}
delete(uri: vscode.Uri, options?: { recursive?: boolean; useTrash?: boolean; }): Promise<void> {
return this._proxy.$delete(uri, { ...{ recursive: false, useTrash: false }, ...options }).catch(ConsumerFileSystem._handleError);
}
rename(oldUri: vscode.Uri, newUri: vscode.Uri, options?: { overwrite?: boolean; }): Promise<void> {
return this._proxy.$rename(oldUri, newUri, { ...{ overwrite: false }, ...options }).catch(ConsumerFileSystem._handleError);
}
copy(source: vscode.Uri, destination: vscode.Uri, options?: { overwrite?: boolean }): Promise<void> {
return this._proxy.$copy(source, destination, { ...{ overwrite: false }, ...options }).catch(ConsumerFileSystem._handleError);
}
private static _handleError(err: any): never {
// generic error
if (!(err instanceof Error)) {
throw new FileSystemError(String(err));
}
// no provider (unknown scheme) error
if (err.name === 'ENOPRO') {
throw FileSystemError.Unavailable(err.message);
}
// file system error
switch (err.name) {
case files.FileSystemProviderErrorCode.FileExists: throw FileSystemError.FileExists(err.message);
case files.FileSystemProviderErrorCode.FileNotFound: throw FileSystemError.FileNotFound(err.message);
case files.FileSystemProviderErrorCode.FileNotADirectory: throw FileSystemError.FileNotADirectory(err.message);
case files.FileSystemProviderErrorCode.FileIsADirectory: throw FileSystemError.FileIsADirectory(err.message);
case files.FileSystemProviderErrorCode.NoPermissions: throw FileSystemError.NoPermissions(err.message);
case files.FileSystemProviderErrorCode.Unavailable: throw FileSystemError.Unavailable(err.message);
default: throw new FileSystemError(err.message, err.name as files.FileSystemProviderErrorCode);
}
}
}
export class ExtHostFileSystem implements ExtHostFileSystemShape {
private readonly _proxy: MainThreadFileSystemShape;
......@@ -172,11 +119,8 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape {
private _linkProviderRegistration?: IDisposable;
private _handlePool: number = 0;
readonly fileSystem: vscode.FileSystem;
constructor(mainContext: IMainContext, private _extHostLanguageFeatures: ExtHostLanguageFeatures) {
this._proxy = mainContext.getProxy(MainContext.MainThreadFileSystem);
this.fileSystem = new ConsumerFileSystem(this._proxy);
// register used schemes
Object.keys(Schemas).forEach(scheme => this._usedSchemes.add(scheme));
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MainThreadFileSystemShape, MainContext } from './extHost.protocol';
import * as vscode from 'vscode';
import * as files from 'vs/platform/files/common/files';
import { FileSystemError } from 'vs/workbench/api/common/extHostTypes';
import { VSBuffer } from 'vs/base/common/buffer';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
export class ExtHostConsumerFileSystem implements vscode.FileSystem {
readonly _serviceBrand: undefined;
private readonly _proxy: MainThreadFileSystemShape;
constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService) {
this._proxy = extHostRpc.getProxy(MainContext.MainThreadFileSystem);
}
stat(uri: vscode.Uri): Promise<vscode.FileStat> {
return this._proxy.$stat(uri).catch(ExtHostConsumerFileSystem._handleError);
}
readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
return this._proxy.$readdir(uri).catch(ExtHostConsumerFileSystem._handleError);
}
createDirectory(uri: vscode.Uri): Promise<void> {
return this._proxy.$mkdir(uri).catch(ExtHostConsumerFileSystem._handleError);
}
async readFile(uri: vscode.Uri): Promise<Uint8Array> {
return this._proxy.$readFile(uri).then(buff => buff.buffer).catch(ExtHostConsumerFileSystem._handleError);
}
writeFile(uri: vscode.Uri, content: Uint8Array): Promise<void> {
return this._proxy.$writeFile(uri, VSBuffer.wrap(content)).catch(ExtHostConsumerFileSystem._handleError);
}
delete(uri: vscode.Uri, options?: { recursive?: boolean; useTrash?: boolean; }): Promise<void> {
return this._proxy.$delete(uri, { ...{ recursive: false, useTrash: false }, ...options }).catch(ExtHostConsumerFileSystem._handleError);
}
rename(oldUri: vscode.Uri, newUri: vscode.Uri, options?: { overwrite?: boolean; }): Promise<void> {
return this._proxy.$rename(oldUri, newUri, { ...{ overwrite: false }, ...options }).catch(ExtHostConsumerFileSystem._handleError);
}
copy(source: vscode.Uri, destination: vscode.Uri, options?: { overwrite?: boolean; }): Promise<void> {
return this._proxy.$copy(source, destination, { ...{ overwrite: false }, ...options }).catch(ExtHostConsumerFileSystem._handleError);
}
private static _handleError(err: any): never {
// generic error
if (!(err instanceof Error)) {
throw new FileSystemError(String(err));
}
// no provider (unknown scheme) error
if (err.name === 'ENOPRO') {
throw FileSystemError.Unavailable(err.message);
}
// file system error
switch (err.name) {
case files.FileSystemProviderErrorCode.FileExists: throw FileSystemError.FileExists(err.message);
case files.FileSystemProviderErrorCode.FileNotFound: throw FileSystemError.FileNotFound(err.message);
case files.FileSystemProviderErrorCode.FileNotADirectory: throw FileSystemError.FileNotADirectory(err.message);
case files.FileSystemProviderErrorCode.FileIsADirectory: throw FileSystemError.FileIsADirectory(err.message);
case files.FileSystemProviderErrorCode.NoPermissions: throw FileSystemError.NoPermissions(err.message);
case files.FileSystemProviderErrorCode.Unavailable: throw FileSystemError.Unavailable(err.message);
default: throw new FileSystemError(err.message, err.name as files.FileSystemProviderErrorCode);
}
}
}
export interface IExtHostConsumerFileSystem extends ExtHostConsumerFileSystem { }
export const IExtHostConsumerFileSystem = createDecorator<IExtHostConsumerFileSystem>('IExtHostConsumerFileSystem');
......@@ -1020,7 +1020,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
if (provider) {
let storageRoot: URI | undefined;
if (this._extensionStoragePaths) {
storageRoot = URI.file(this._extensionStoragePaths.workspaceValue(provider.extension) ?? this._extensionStoragePaths.globalValue(provider.extension));
storageRoot = this._extensionStoragePaths.workspaceValue(provider.extension) ?? this._extensionStoragePaths.globalValue(provider.extension);
}
let document = this._documents.get(URI.revive(uri).toString());
......@@ -1330,7 +1330,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
const entry = this._notebookContentProviders.get(viewType);
let storageRoot: URI | undefined;
if (entry && this._extensionStoragePaths) {
storageRoot = URI.file(this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension));
storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension);
}
if (!this._documents.has(revivedUriStr)) {
......
......@@ -5,12 +5,83 @@
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { ILogService } from 'vs/platform/log/common/log';
import { IEnvironment, IStaticWorkspaceData } from 'vs/workbench/api/common/extHost.protocol';
import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
import { URI } from 'vs/base/common/uri';
export const IExtensionStoragePaths = createDecorator<IExtensionStoragePaths>('IExtensionStoragePaths');
export interface IExtensionStoragePaths {
readonly _serviceBrand: undefined;
whenReady: Promise<any>;
workspaceValue(extension: IExtensionDescription): string | undefined;
globalValue(extension: IExtensionDescription): string;
workspaceValue(extension: IExtensionDescription): URI | undefined;
globalValue(extension: IExtensionDescription): URI;
}
export class ExtensionStoragePaths implements IExtensionStoragePaths {
readonly _serviceBrand: undefined;
private readonly _workspace?: IStaticWorkspaceData;
private readonly _environment: IEnvironment;
readonly whenReady: Promise<URI | undefined>;
private _value?: URI;
constructor(
@IExtHostInitDataService initData: IExtHostInitDataService,
@ILogService private readonly _logService: ILogService,
@IExtHostConsumerFileSystem private readonly _extHostFileSystem: IExtHostConsumerFileSystem
) {
this._workspace = initData.workspace ?? undefined;
this._environment = initData.environment;
this.whenReady = this._getOrCreateWorkspaceStoragePath().then(value => this._value = value);
}
private async _getOrCreateWorkspaceStoragePath(): Promise<URI | undefined> {
if (!this._workspace) {
return Promise.resolve(undefined);
}
const storageName = this._workspace.id;
const storageUri = URI.joinPath(this._environment.workspaceStorageHome, storageName);
try {
await this._extHostFileSystem.stat(storageUri);
this._logService.trace('[ExtHostStorage] storage dir already exists', storageUri);
return storageUri;
} catch {
// doesn't exist, that's OK
}
try {
this._logService.trace('[ExtHostStorage] creating dir and metadata-file', storageUri);
await this._extHostFileSystem.createDirectory(storageUri);
await this._extHostFileSystem.writeFile(
URI.joinPath(storageUri, 'meta.json'),
new TextEncoder().encode(JSON.stringify({
id: this._workspace.id,
configuration: URI.revive(this._workspace.configuration)?.toString(),
name: this._workspace.name
}, undefined, 2))
);
return storageUri;
} catch (e) {
this._logService.error('[ExtHostStorage]', e);
return undefined;
}
}
workspaceValue(extension: IExtensionDescription): URI | undefined {
if (this._value) {
return URI.joinPath(this._value, extension.identifier.value);
}
return undefined;
}
globalValue(extension: IExtensionDescription): URI {
return URI.joinPath(this._environment.globalStorageHome, extension.identifier.value.toLowerCase());
}
}
......@@ -598,7 +598,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
let storageRoot: URI | undefined;
if (this.supportEditing(entry.provider) && this._extensionStoragePaths) {
storageRoot = URI.file(this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension));
storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension);
}
this._documents.add(viewType, document, storageRoot);
......
......@@ -19,8 +19,7 @@ import { ExtHostDebugService } from 'vs/workbench/api/node/extHostDebugService';
import { IExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService';
import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch';
import { NativeExtHostSearch } from 'vs/workbench/api/node/extHostSearch';
import { ExtensionStoragePaths } from 'vs/workbench/api/node/extHostStoragePaths';
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import { IExtensionStoragePaths, ExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';
import { IExtHostStorage, ExtHostStorage } from 'vs/workbench/api/common/extHostStorage';
......@@ -30,6 +29,7 @@ import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelServ
import { ExtHostTunnelService } from 'vs/workbench/api/node/extHostTunnelService';
import { IExtHostApiDeprecationService, ExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
import { IExtHostWindow, ExtHostWindow } from 'vs/workbench/api/common/extHostWindow';
import { IExtHostConsumerFileSystem, ExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
// register singleton services
registerSingleton(ILogService, ExtHostLogService);
......@@ -45,6 +45,7 @@ registerSingleton(IExtHostTerminalService, ExtHostTerminalService);
registerSingleton(IExtHostTask, ExtHostTask);
registerSingleton(IExtHostDebugService, ExtHostDebugService);
registerSingleton(IExtHostSearch, NativeExtHostSearch);
registerSingleton(IExtHostConsumerFileSystem, ExtHostConsumerFileSystem);
registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths);
registerSingleton(IExtHostExtensionService, ExtHostExtensionService);
registerSingleton(IExtHostStorage, ExtHostStorage);
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import * as pfs from 'vs/base/node/pfs';
import { IEnvironment, IStaticWorkspaceData } from 'vs/workbench/api/common/extHost.protocol';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { withNullAsUndefined } from 'vs/base/common/types';
import { ILogService } from 'vs/platform/log/common/log';
export class ExtensionStoragePaths implements IExtensionStoragePaths {
readonly _serviceBrand: undefined;
private readonly _workspace?: IStaticWorkspaceData;
private readonly _environment: IEnvironment;
readonly whenReady: Promise<string | undefined>;
private _value?: string;
constructor(
@IExtHostInitDataService initData: IExtHostInitDataService,
@ILogService private readonly _logService: ILogService,
) {
this._workspace = withNullAsUndefined(initData.workspace);
this._environment = initData.environment;
this.whenReady = this._getOrCreateWorkspaceStoragePath().then(value => this._value = value);
}
workspaceValue(extension: IExtensionDescription): string | undefined {
if (this._value) {
return path.join(this._value, extension.identifier.value);
}
return undefined;
}
globalValue(extension: IExtensionDescription): string {
return path.join(this._environment.globalStorageHome.fsPath, extension.identifier.value.toLowerCase());
}
private async _getOrCreateWorkspaceStoragePath(): Promise<string | undefined> {
if (!this._workspace) {
return Promise.resolve(undefined);
}
if (!this._environment.appSettingsHome) {
return undefined;
}
const storageName = this._workspace.id;
const storagePath = path.join(this._environment.appSettingsHome.fsPath, 'workspaceStorage', storageName);
const exists = await pfs.dirExists(storagePath);
if (exists) {
return storagePath;
}
try {
await pfs.mkdirp(storagePath);
await pfs.writeFile(
path.join(storagePath, 'meta.json'),
JSON.stringify({
id: this._workspace.id,
configuration: this._workspace.configuration && URI.revive(this._workspace.configuration).toString(),
name: this._workspace.name
}, undefined, 2)
);
return storagePath;
} catch (e) {
this._logService.error(e);
return undefined;
}
}
}
......@@ -14,7 +14,7 @@ import { IExtHostTerminalService, WorkerExtHostTerminalService } from 'vs/workbe
import { IExtHostTask, WorkerExtHostTask } from 'vs/workbench/api/common/extHostTask';
import { IExtHostDebugService, WorkerExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService';
import { IExtHostSearch, ExtHostSearch } from 'vs/workbench/api/common/extHostSearch';
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import { IExtensionStoragePaths, ExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
import { IExtHostStorage, ExtHostStorage } from 'vs/workbench/api/common/extHostStorage';
import { ExtHostExtensionService } from 'vs/workbench/api/worker/extHostExtensionService';
......@@ -23,7 +23,7 @@ import { ExtHostLogService } from 'vs/workbench/api/worker/extHostLogService';
import { IExtHostTunnelService, ExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
import { IExtHostApiDeprecationService, ExtHostApiDeprecationService, } from 'vs/workbench/api/common/extHostApiDeprecationService';
import { IExtHostWindow, ExtHostWindow } from 'vs/workbench/api/common/extHostWindow';
import { NotImplementedProxy } from 'vs/base/common/types';
import { ExtHostConsumerFileSystem, IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
// register singleton services
registerSingleton(ILogService, ExtHostLogService);
......@@ -39,8 +39,9 @@ registerSingleton(IExtHostStorage, ExtHostStorage);
registerSingleton(IExtHostExtensionService, ExtHostExtensionService);
registerSingleton(IExtHostSearch, ExtHostSearch);
registerSingleton(IExtHostTunnelService, ExtHostTunnelService);
registerSingleton(IExtHostConsumerFileSystem, ExtHostConsumerFileSystem);
registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths);
registerSingleton(IExtHostTerminalService, WorkerExtHostTerminalService);
registerSingleton(IExtHostTask, WorkerExtHostTask);
registerSingleton(IExtHostDebugService, WorkerExtHostDebugService);
registerSingleton(IExtensionStoragePaths, class extends NotImplementedProxy<IExtensionStoragePaths>(String(IExtensionStoragePaths)) { whenReady = Promise.resolve(); });
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册