Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,33 @@
<h6><i class="fa fa-paperclip">&nbsp;</i>{{'item.page.files.head' | translate}}</h6>
<div class="pb-3">
<span class="pr-1">
<a class="btn btn-download" (click)="setCommandline()" style="text-decoration: none"
<a class="btn btn-download" (click)="openCommandModal(commandModal)" style="text-decoration: none"
*ngIf="canShowCurlDownload">
<i class="fa fa-download fa-3x" style="display: block">&nbsp;</i>
{{'item.page.download.button.command.line' | translate}}
</a>
</span>
<div id="command-div" *ngIf="isCommandLineVisible">
<pre class="command-pre">{{ command }}</pre>
</div>

<ng-template #commandModal let-modal>
<div class="modal-header">
<h5 class="modal-title" id="commandModalTitle">{{'item.page.download.button.command.line' | translate}}</h5>
<button type="button" class="close" aria-label="Close" (click)="modal.dismiss()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<pre class="command-pre mb-0">{{ command }}</pre>
</div>
<div class="modal-footer">
<button class="btn btn-outline-secondary" (click)="copyCommand()">
<i class="fa" [ngClass]="commandCopied ? 'fa-check' : 'fa-clipboard'"></i>
{{ commandCopied ? ('item.page.download.command.copied' | translate) : ('item.page.download.command.copy' | translate) }}
</button>
<button class="btn btn-secondary" (click)="modal.close()">
{{'item.page.download.command.close' | translate}}
</button>
</div>
</ng-template>
<span>
<a *ngIf="
(totalFileSizes | async) > (downloadZipMinFileSize | async) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,17 @@ The styling file for the `clarin-files-section.component`

.command-pre {
display: block;
padding: 9.5px;
margin: 0 0 10px;
padding: 12px 16px;
margin: 0;
font-size: 13px;
line-height: 1.428571429;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
background-color: #d9edf7;
color: #3a87ad;
border: 1px solid #ccc;
border-radius: 4px;
}

#command-div .repo-copy-btn {
opacity: 0;
-webkit-transition: opacity 0.3s ease-in-out;
-o-transition: opacity 0.3s ease-in-out;
transition: opacity 0.3s ease-in-out;
}

#command-div:hover .repo-copy-btn, #command-div .repo-copy-btn:focus {
opacity: 1;
}

.repo-copy-btn {
width: 20px;
height: 20px;
position: relative;
display: inline-block;
padding: 0 !important;
}

.repo-copy-btn:before {
content: " ";
background-size: 13px 15px;
background-repeat: no-repeat;
background-color: red;
display: inline-block;
width: 13px;
height: 15px;
padding: 0 !important;
max-height: 60vh;
overflow-y: auto;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,39 @@ import { ConfigurationDataService } from '../../core/data/configuration-data.ser
import { Item } from '../../core/shared/item.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';

describe('ClarinFilesSectionComponent', () => {
let component: ClarinFilesSectionComponent;
let fixture: ComponentFixture<ClarinFilesSectionComponent>;

let mockRegistryService: any;
let halService: HALEndpointService;
let halService: any;

const ROOT_HREF = 'http://localhost:8080/server/api';

function createMetadataBitstream(name: string, canPreview: boolean = true): MetadataBitstream {
const bs = new MetadataBitstream();
bs.id = '70ccc608-f6a5-4c96-ab2d-53bc56ae8ebe';
bs.name = name;
bs.description = 'test';
bs.fileSize = 1024;
bs.checksum = 'abc';
bs.type = new ResourceType('item');
bs.fileInfo = [];
bs.format = 'text';
bs.canPreview = canPreview;
bs._links = {
self: new HALLink(),
schema: new HALLink(),
};
bs._links.self.href = '';
bs._links.schema.href = '';
return bs;
}

// Set up the mock service's getMetadataBitstream method to return a simple stream
const metadatabitstream = new MetadataBitstream();
metadatabitstream.id = '70ccc608-f6a5-4c96-ab2d-53bc56ae8ebe';
metadatabitstream.name = 'test';
metadatabitstream.description = 'test';
metadatabitstream.fileSize = 1024;
metadatabitstream.checksum = 'abc';
metadatabitstream.type = new ResourceType('item');
metadatabitstream.fileInfo = [];
metadatabitstream.format = 'text';
metadatabitstream.canPreview = false;
metadatabitstream._links = {
self: new HALLink(),
schema: new HALLink(),
};

metadatabitstream._links.self.href = '';
metadatabitstream._links.schema.href = '';
const metadatabitstream = createMetadataBitstream('test', false);
const metadataBitstreams: MetadataBitstream[] = [metadatabitstream];
const bitstreamStream = new BehaviorSubject(metadataBitstreams);

Expand All @@ -55,20 +63,23 @@ describe('ClarinFilesSectionComponent', () => {
});

const configurationServiceSpy = jasmine.createSpyObj('configurationService', {
findByPropertyName: of('123456'),
findByPropertyName: createSuccessfulRemoteDataObject$({ values: ['123456'] }),
});

beforeEach(async () => {
mockRegistryService = jasmine.createSpyObj('RegistryService', {
'getMetadataBitstream': of(bitstreamStream)
}
);
halService = Object.assign(new HALEndpointServiceStub('some url'));
halService = Object.assign(new HALEndpointServiceStub('some url'), {
getRootHref: () => ROOT_HREF
});

await TestBed.configureTestingModule({
declarations: [ ClarinFilesSectionComponent ],
imports: [
TranslateModule.forRoot()
TranslateModule.forRoot(),
NgbModalModule,
],
providers: [
{ provide: RegistryService, useValue: mockRegistryService },
Expand All @@ -88,4 +99,142 @@ describe('ClarinFilesSectionComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

describe('generateCurlCommand', () => {
const BASE = `${ROOT_HREF}/core/bitstreams/handle`;

it('should generate a curl command for a single file', () => {
component.itemHandle = '123456789/1';
component.listOfFiles.next([createMetadataBitstream('simple.txt')]);
component.generateCurlCommand();
expect(component.command).toBe(
`curl -o "simple.txt" "${BASE}/123456789/1/simple.txt"`
);
});

it('should generate a curl command for multiple files', () => {
component.itemHandle = '123456789/2';
component.listOfFiles.next([
createMetadataBitstream('file1.txt'),
createMetadataBitstream('file2.txt'),
]);
component.generateCurlCommand();
expect(component.command).toBe(
`curl -o "file1.txt" "${BASE}/123456789/2/file1.txt" ` +
`-o "file2.txt" "${BASE}/123456789/2/file2.txt"`
);
});

it('should percent-encode spaces in URL but keep real name in -o', () => {
component.itemHandle = '123456789/3';
component.listOfFiles.next([createMetadataBitstream('my file.txt')]);
component.generateCurlCommand();
expect(component.command).toBe(
`curl -o "my file.txt" "${BASE}/123456789/3/my%20file.txt"`
);
});

it('should percent-encode parentheses in URL but keep real name in -o', () => {
component.itemHandle = '123456789/4';
component.listOfFiles.next([createMetadataBitstream('logo (2).png')]);
component.generateCurlCommand();
expect(component.command).toBe(
`curl -o "logo (2).png" "${BASE}/123456789/4/logo%20%282%29.png"`
);
});

it('should percent-encode plus signs in URL', () => {
component.itemHandle = '123456789/5';
component.listOfFiles.next([createMetadataBitstream('dtq+logo.png')]);
component.generateCurlCommand();
expect(component.command).toBe(
`curl -o "dtq+logo.png" "${BASE}/123456789/5/dtq%2Blogo.png"`
);
});

it('should handle mixed special characters in multiple files', () => {
component.itemHandle = '123456789/6';
component.listOfFiles.next([
createMetadataBitstream('dtq+logo (2).png'),
createMetadataBitstream('Screenshot 1.png'),
]);
component.generateCurlCommand();
expect(component.command).toBe(
`curl -o "dtq+logo (2).png" "${BASE}/123456789/6/dtq%2Blogo%20%282%29.png" ` +
`-o "Screenshot 1.png" "${BASE}/123456789/6/Screenshot%201.png"`
);
});

it('should preserve UTF-8 characters in -o filename and encode in URL', () => {
component.itemHandle = '123456789/9';
component.listOfFiles.next([createMetadataBitstream('M\u00e9di\u00e1 (3).jfif')]);
component.generateCurlCommand();
expect(component.command).toBe(
`curl -o "M\u00e9di\u00e1 (3).jfif" "${BASE}/123456789/9/M%C3%A9di%C3%A1%20%283%29.jfif"`
);
});

it('should escape double quotes in filenames', () => {
component.itemHandle = '123456789/10';
component.listOfFiles.next([createMetadataBitstream('file "quoted".txt')]);
component.generateCurlCommand();
expect(component.command).toBe(
`curl -o "file \\"quoted\\".txt" "${BASE}/123456789/10/file%20%22quoted%22.txt"`
);
});

it('should set canShowCurlDownload to true when any file canPreview', () => {
component.canShowCurlDownload = false;
component.itemHandle = '123456789/7';
component.listOfFiles.next([createMetadataBitstream('file.txt', true)]);
component.generateCurlCommand();
expect(component.canShowCurlDownload).toBeTrue();
});

it('should not set canShowCurlDownload for non-previewable files', () => {
component.canShowCurlDownload = false;
component.itemHandle = '123456789/8';
component.listOfFiles.next([createMetadataBitstream('file.txt', false)]);
component.generateCurlCommand();
expect(component.canShowCurlDownload).toBeFalse();
});

it('should handle filenames containing a literal percent sign', () => {
component.itemHandle = '123456789/11';
component.listOfFiles.next([createMetadataBitstream('100% done.txt')]);
component.generateCurlCommand();
expect(component.command).toBe(
`curl -o "100% done.txt" "${BASE}/123456789/11/100%25%20done.txt"`
);
});

it('should handle complex filename with diacritics, plus, hash, and unmatched paren', () => {
component.itemHandle = '123456789/12';
component.listOfFiles.next([createMetadataBitstream('M\u00e9di\u00e1 (+)#9) ano')]);
component.generateCurlCommand();
expect(component.command).toBe(
`curl -o "M\u00e9di\u00e1 (+)#9) ano" "${BASE}/123456789/12/M%C3%A9di%C3%A1%20%28%2B%29%239%29%20ano"`
);
});

it('should reset canShowCurlDownload when called again with non-previewable files', () => {
component.itemHandle = '123456789/13';
component.listOfFiles.next([createMetadataBitstream('file.txt', true)]);
component.generateCurlCommand();
expect(component.canShowCurlDownload).toBeTrue();
// Now call again with non-previewable files
component.listOfFiles.next([createMetadataBitstream('file.txt', false)]);
component.generateCurlCommand();
expect(component.canShowCurlDownload).toBeFalse();
});

it('should escape dollar signs and backticks in filenames for shell safety', () => {
component.itemHandle = '123456789/14';
component.listOfFiles.next([createMetadataBitstream('price$100.txt')]);
component.generateCurlCommand();
expect(component.command).toBe(
`curl -o "price\\$100.txt" "${BASE}/123456789/14/price%24100.txt"`
);
});
});
});
Loading