Repository: ShareDropio/sharedrop
Branch: master
Commit: 2b6a9fe5e6ca
Files: 93
Total size: 131.4 KB
Directory structure:
gitextract_vxj0u4do/
├── .editorconfig
├── .ember-cli
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .template-lintrc.js
├── .travis.yml
├── .watchmanconfig
├── Dockerfile
├── LICENSE
├── Procfile
├── Procfile.dev
├── README.md
├── app/
│ ├── app.js
│ ├── components/
│ │ ├── circular-progress.hbs
│ │ ├── circular-progress.js
│ │ ├── file-field.js
│ │ ├── modal-dialog.js
│ │ ├── peer-avatar.js
│ │ ├── peer-widget.js
│ │ ├── popover-confirm.js
│ │ ├── room-url.js
│ │ └── user-widget.js
│ ├── controllers/
│ │ ├── application.js
│ │ └── index.js
│ ├── helpers/
│ │ └── is-equal.js
│ ├── index.html
│ ├── initializers/
│ │ └── prerequisites.js
│ ├── models/
│ │ ├── peer.js
│ │ └── user.js
│ ├── router.js
│ ├── routes/
│ │ ├── application.js
│ │ ├── error.js
│ │ ├── index.js
│ │ └── room.js
│ ├── services/
│ │ ├── analytics.js
│ │ ├── avatar.js
│ │ ├── file.js
│ │ ├── room.js
│ │ └── web-rtc.js
│ ├── styles/
│ │ ├── app.sass
│ │ ├── base/
│ │ │ ├── _element_defaults.sass
│ │ │ ├── _glyphicons_filetypes.sass
│ │ │ ├── _mixins.sass
│ │ │ ├── _reset.sass
│ │ │ └── _variables.sass
│ │ ├── layout/
│ │ │ ├── _content.sass
│ │ │ ├── _footer.sass
│ │ │ ├── _header.sass
│ │ │ └── _media.sass
│ │ └── modules/
│ │ ├── _modal.sass
│ │ ├── _modules.sass
│ │ ├── _popover.sass
│ │ └── _users.sass
│ └── templates/
│ ├── about-app.hbs
│ ├── about-room.hbs
│ ├── about-you.hbs
│ ├── application.hbs
│ ├── components/
│ │ ├── modal-dialog.hbs
│ │ ├── peer-widget.hbs
│ │ ├── popover-confirm.hbs
│ │ └── user-widget.hbs
│ ├── errors/
│ │ ├── browser-unsupported.hbs
│ │ ├── filesystem-unavailable.hbs
│ │ └── popovers/
│ │ ├── connection-failed.hbs
│ │ └── multiple-files.hbs
│ └── index.hbs
├── config/
│ ├── dotenv.js
│ ├── environment.js
│ ├── optional-features.json
│ └── targets.js
├── ember-cli-build.js
├── firebase_rules.json
├── lib/
│ └── google-analytics/
│ ├── index.js
│ └── package.json
├── newrelic.js
├── package.json
├── prettier.config.js
├── public/
│ ├── .gitkeep
│ ├── .well-known/
│ │ └── brave-rewards-verification.txt
│ ├── crossdomain.xml
│ └── robots.txt
├── server.js
├── sharedrop.crx
├── testem.js
├── tests/
│ ├── helpers/
│ │ └── .gitkeep
│ ├── index.html
│ ├── integration/
│ │ └── .gitkeep
│ ├── test-helper.js
│ └── unit/
│ └── .gitkeep
└── vendor/
├── .gitkeep
└── peer.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
[*.hbs]
insert_final_newline = false
[*.{diff,md}]
trim_trailing_whitespace = false
================================================
FILE: .ember-cli
================================================
{
/**
Ember CLI sends analytics information by default. The data is completely
anonymous, but there are times when you might want to disable this behavior.
Setting `disableAnalytics` to true will prevent any data from being sent.
*/
"disableAnalytics": false
}
================================================
FILE: .eslintignore
================================================
# unconventional js
/blueprints/*/files/
/vendor/
# compiled output
/dist/
/tmp/
# dependencies
/bower_components/
/node_modules/
# misc
/coverage/
!.*
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try
================================================
FILE: .eslintrc.js
================================================
const eslintPluginNode = require('eslint-plugin-node');
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
legacyDecorators: true,
},
},
plugins: ['ember', 'prettier'],
extends: ['airbnb-base', 'plugin:ember/recommended', 'prettier'],
env: {
browser: true,
},
rules: {
'func-names': 'off',
'no-console': 'off',
'no-underscore-dangle': 'off',
'import/no-extraneous-dependencies': 'off',
'import/no-unresolved': 'off',
'prettier/prettier': 'error',
// TODO: Enable these
'ember/no-jquery': 'off',
'ember/no-observers': 'off',
'ember/no-get': 'off',
},
overrides: [
// node files
{
files: [
'.eslintrc.js',
'.template-lintrc.js',
'ember-cli-build.js',
'testem.js',
'blueprints/*/index.js',
'config/**/*.js',
'lib/*/index.js',
'server/**/*.js',
],
parserOptions: {
sourceType: 'script',
},
env: {
browser: false,
node: true,
},
plugins: ['node'],
rules: {
...eslintPluginNode.configs.recommended.rules,
// add your custom rules and overrides for node files here
// this can be removed once the following is fixed
// https://github.com/mysticatea/eslint-plugin-node/issues/77
'node/no-unpublished-require': 'off',
},
},
],
};
================================================
FILE: .gitignore
================================================
# See https://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist/
/tmp/
# dependencies
/bower_components/
/node_modules/
# misc
/.env*
/.pnp*
/.sass-cache
/connect.lock
/coverage/
/libpeerconnection.log
/npm-debug.log*
/testem.log
/yarn-error.log
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try
================================================
FILE: .prettierignore
================================================
##########
# Common
##########
# Duh
.git/
# Third party
/node_modules/
/vendor/
# Build products
/dist/
/tmp/
/coverage/
/.sass-cache/
================================================
FILE: .template-lintrc.js
================================================
module.exports = {
extends: 'octane',
// TODO: enable these
rules: {
'no-action': false,
'no-curly-component-invocation': false,
'no-implicit-this': false,
'no-partial': false,
},
};
================================================
FILE: .travis.yml
================================================
---
language: node_js
node_js:
- "12"
dist: xenial
addons:
chrome: stable
cache:
yarn: true
env:
global:
# See https://git.io/vdao3 for details.
- JOBS=1
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH=$HOME/.yarn/bin:$PATH
script:
- yarn test
================================================
FILE: .watchmanconfig
================================================
{
"ignore_dirs": [
"tmp",
"dist"
]
}
================================================
FILE: Dockerfile
================================================
FROM node:14-buster
RUN mkdir -p /srv/app
WORKDIR /srv/app
COPY package.json yarn.lock ./
RUN yarn --frozen-lockfile --non-interactive
COPY . /srv/app
EXPOSE 8000
CMD [ "yarn", "develop" ]
================================================
FILE: LICENSE
================================================
The MIT License
Copyright (c) 2014-2024 Szymon Nowak
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: Procfile
================================================
web: node server.js
================================================
FILE: Procfile.dev
================================================
server: node server.js
web: ember build --watch
================================================
FILE: README.md
================================================
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/9873/200137755-47fe06a7-3c37-439f-b44c-cee9168418fa.svg">
<img alt="ShareDrop" src="https://user-images.githubusercontent.com/9873/200137972-bfd145a2-36eb-48ad-8568-53184d3599c1.svg">
</picture>
# ShareDrop is now LimeWire
Dear ShareDrop community,
ShareDrop has been acquired by LimeWire, a leading file sharing platform with integrated AI tools. You can continue to share any files between devices, while benefitting from:
* sharing files between devices in the same network
* anonymous up- & downloads
* end-to-end encryption
* up to 40GB storage for free users
* integrated AI tools for signed-up users
Visit [sharedrop.io](https://sharedrop.io) or [limewire.com](https://limewire.com)
The Github repository will stay as-is and you can still go ahead and download and run the classic ShareDrop on your own infrastructure.
# ShareDrop Classic
ShareDrop is a web application inspired by Apple [AirDrop](http://support.apple.com/kb/ht4783) service. It allows you to transfer files directly between devices, without having to upload them to any server first. It uses [WebRTC](http://www.webrtc.org) for secure peer-to-peer file transfer and [Firebase](https://www.firebase.com) for presence management and WebRTC signaling.
ShareDrop allows you to send files to other devices in the same local network (i.e. devices with the same public IP address) without any configuration - simply open <https://www.sharedrop.io> on all devices and they will see each other. It also allows you to send files between networks - just click the `+` button in the top right corner of the page to create a room with a unique URL and share this URL with other people you want to send a file to. Once they open this page in a browser on their devices, you'll see each other's avatars.
The main difference between ShareDrop and AirDrop is that ShareDrop requires Internet connection to discover other devices, while AirDrop doesn't need one, as it creates ad-hoc wireless network between them. On the other hand, ShareDrop allows you to share files between mobile (Android and iOS) and desktop devices and even between networks.
## Supported browsers
- Chrome
- Edge (Chromium based)
- Firefox
- Opera
- Safari 13+
## Local development
1. Setup Firebase:
1. [Sign up](https://www.firebase.com) for a Firebase account and create a database.
2. Go to "Security Rules" tab, click "Load Rules" button and select `firebase_rules.json` file.
3. Take note of your database URL and its secret, which can be found in "Secrets" tab.
2. Run `npm install -g ember-cli` to install Ember CLI.
3. Run `yarn` to install app dependencies.
4. Run `cp .env{.sample,}` to create `.env` file. This file will be used by Foreman to set environment variables when running the app locally.
- `SECRET` key is used to encrypt cookies and generate room name based on public IP address for `/` route. It can be any random string - you can generate one using e.g. `date | md5sum`
- `NEW_RELIC_*` keys are only necessary in production
5. Run `yarn develop` to start the app.
## Deployment
### Heroku
Create a new Heroku app:
```
heroku create <app-name>
```
and push the app to Heroku repo:
```
git push heroku master
```
================================================
FILE: app/app.js
================================================
import Application from '@ember/application';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from 'sharedrop/config/environment';
import * as Sentry from '@sentry/browser';
import { Ember as EmberIntegration } from '@sentry/integrations';
Sentry.init({
dsn:
'https://ba1292a9c759401dbbda4272f183408d@o432021.ingest.sentry.io/5384091',
integrations: [new EmberIntegration()],
});
export default class App extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
}
loadInitializers(App, config.modulePrefix);
================================================
FILE: app/components/circular-progress.hbs
================================================
<svg width="76" height="76" viewport="0 0 76 76" style={{this.style}}>
<path class="break" transform="translate(38, 38)" d={{this.path}} />
</svg>
================================================
FILE: app/components/circular-progress.js
================================================
import Component from '@glimmer/component';
import { htmlSafe } from '@ember/template';
const COLORS = {
blue: '0, 136, 204',
orange: '197, 197, 51',
};
export default class CircularProgress extends Component {
constructor(owner, args) {
super(owner, args);
const rgb = COLORS[args.color];
this.style = htmlSafe(`fill: rgba(${rgb}, .5)`);
}
get path() {
const π = Math.PI;
const α = this.args.value * 360;
const r = (α * π) / 180;
const mid = α > 180 ? 1 : 0;
const x = Math.sin(r) * 38;
const y = Math.cos(r) * -38;
return `M 0 0 v -38 A 38 38 1 ${mid} 1 ${x} ${y} z`;
}
}
================================================
FILE: app/components/file-field.js
================================================
import TextField from '@ember/component/text-field';
import $ from 'jquery';
export default TextField.extend({
type: 'file',
classNames: ['invisible'],
click(event) {
event.stopPropagation();
},
change(event) {
const input = event.target;
const { files } = input;
this.onChange({ files });
this.reset();
},
// Hackish way to reset file input when sender cancels file transfer,
// so if sender wants later to send the same file again,
// the 'change' event is triggered correctly.
reset() {
const field = $(this.element);
field.wrap('<form>').closest('form').get(0).reset();
field.unwrap();
},
});
================================================
FILE: app/components/modal-dialog.js
================================================
import Component from '@ember/component';
export default Component.extend({
actions: {
close() {
// This sends an action to application route.
// eslint-disable-next-line ember/closure-actions
return this.onClose();
},
},
});
================================================
FILE: app/components/peer-avatar.js
================================================
import Component from '@ember/component';
import { alias } from '@ember/object/computed';
import { later } from '@ember/runloop';
import $ from 'jquery';
export default Component.extend({
tagName: 'img',
classNames: ['gravatar'],
attributeBindings: [
'src',
'alt',
'title',
'data-sending-progress',
'data-receiving-progress',
],
src: alias('peer.avatarUrl'),
alt: alias('peer.label'),
title: alias('peer.uuid'),
'data-sending-progress': alias('peer.transfer.sendingProgress'),
'data-receiving-progress': alias('peer.transfer.receivingProgress'),
toggleTransferCompletedClass() {
const className = 'transfer-completed';
later(
this,
function toggleClass() {
$(this.element)
.parent('.avatar')
.addClass(className)
.delay(2000)
.queue(function removeClass() {
$(this).removeClass(className).dequeue();
});
},
250,
);
},
init(...args) {
this._super(args);
this.toggleTransferCompletedClass = this.toggleTransferCompletedClass.bind(
this,
);
},
didInsertElement(...args) {
this._super(args);
const { peer } = this;
peer.on('didReceiveFile', this.toggleTransferCompletedClass);
peer.on('didSendFile', this.toggleTransferCompletedClass);
},
willDestroyElement(...args) {
this._super(args);
const { peer } = this;
peer.off('didReceiveFile', this.toggleTransferCompletedClass);
peer.off('didSendFile', this.toggleTransferCompletedClass);
},
// Delegate click to hidden file field in peer template
click() {
if (this.canSendFile()) {
$(this.element).closest('.peer').find('input[type=file]').click();
}
},
// Handle drop events
dragEnter(event) {
this.cancelEvent(event);
$(this.element).parent('.avatar').addClass('hover');
},
dragOver(event) {
this.cancelEvent(event);
},
dragLeave() {
$(this.element).parent('.avatar').removeClass('hover');
},
drop(event) {
this.cancelEvent(event);
$(this.element).parent('.avatar').removeClass('hover');
const { peer } = this;
const dt = event.originalEvent.dataTransfer;
const { files } = dt;
if (this.canSendFile()) {
if (!this.isTransferableBundle(files)) {
peer.setProperties({
state: 'error',
errorCode: 'multiple-files',
});
} else {
this.onFileDrop({ files });
}
}
},
cancelEvent(event) {
event.stopPropagation();
event.preventDefault();
},
canSendFile() {
const { peer } = this;
// Can't send files if another file transfer is already in progress
return !(
peer.get('state') === 'is_preparing_file_transfer' ||
peer.get('transfer.file') ||
peer.get('transfer.info')
);
},
isTransferableBundle(files) {
if (files.length === 1 && files[0] instanceof window.File) return true;
const fileSizeLimit = 50 * 1024 * 1024;
const bundleSizeLimit = 200 * 1024 * 1024;
let aggregatedSize = 0;
// eslint-disable-next-line no-restricted-syntax
for (const file of files) {
if (!(file instanceof window.File)) {
return false;
}
if (file.size > fileSizeLimit) {
return false;
}
aggregatedSize += file.size;
if (aggregatedSize > bundleSizeLimit) {
return false;
}
}
return true;
},
});
================================================
FILE: app/components/peer-widget.js
================================================
import Component from '@ember/component';
import { computed } from '@ember/object';
import { alias, equal, gt } from '@ember/object/computed';
import JSZip from 'jszip';
export default Component.extend({
classNames: ['peer'],
classNameBindings: ['peer.peer.state'],
peer: null,
hasCustomRoomName: false,
webrtc: null, // TODO inject webrtc as a service
label: alias('peer.label'),
isIdle: equal('peer.state', 'idle'),
isPreparingFileTransfer: equal('peer.state', 'is_preparing_file_transfer'),
hasSelectedFile: equal('peer.state', 'has_selected_file'),
isSendingFileInfo: equal('peer.state', 'sending_file_info'),
isAwaitingFileInfo: equal('peer.state', 'awaiting_file_info'),
isAwaitingResponse: equal('peer.state', 'awaiting_response'),
hasReceivedFileInfo: equal('peer.state', 'received_file_info'),
hasDeclinedFileTransfer: equal('peer.state', 'declined_file_transfer'),
hasError: equal('peer.state', 'error'),
isReceivingFile: gt('peer.transfer.receivingProgress', 0),
isSendingFile: gt('peer.transfer.sendingProgress', 0),
filename: computed('peer.transfer.{file,info}', function () {
const file = this.get('peer.transfer.file');
const info = this.get('peer.transfer.info');
if (file) {
return file.name;
}
if (info) {
return info.name;
}
return null;
}),
errorTemplateName: computed('peer.errorCode', function () {
const errorCode = this.get('peer.errorCode');
return errorCode ? `errors/popovers/${errorCode}` : null;
}),
actions: {
// TODO: rename to something more meaningful (e.g. askIfWantToSendFile)
uploadFile(data) {
const { peer } = this;
const { files } = data;
peer.set('state', 'is_preparing_file_transfer');
peer.set('bundlingProgress', 0);
// Cache the file, so that it's available
// when the response from the recipient comes in
this._reduceFiles(files).then((file) => {
peer.set('transfer.file', file);
peer.set('state', 'has_selected_file');
});
},
sendFileTransferInquiry() {
const { webrtc } = this;
const { peer } = this;
webrtc.connect(peer.get('peer.id'));
peer.set('state', 'establishing_connection');
},
cancelFileTransfer() {
this._cancelFileTransfer();
},
abortFileTransfer() {
this._cancelFileTransfer();
const { webrtc } = this;
const connection = this.get('peer.peer.connection');
webrtc.sendCancelRequest(connection);
},
acceptFileTransfer() {
const { peer } = this;
this._sendFileTransferResponse(true);
peer.get('peer.connection').on('receiving_progress', (progress) => {
peer.set('transfer.receivingProgress', progress);
});
peer.set('state', 'sending_file_data');
},
rejectFileTransfer() {
const { peer } = this;
this._sendFileTransferResponse(false);
peer.set('transfer.info', null);
peer.set('state', 'idle');
},
},
_cancelFileTransfer() {
const { peer } = this;
peer.setProperties({
'transfer.file': null,
state: 'idle',
});
},
_sendFileTransferResponse(response) {
const { webrtc } = this;
const { peer } = this;
const connection = peer.get('peer.connection');
webrtc.sendFileResponse(connection, response);
},
async _reduceFiles(files) {
const { peer } = this;
if (files.length === 1) {
return Promise.resolve(files[0]);
}
const zip = new JSZip();
Array.prototype.forEach.call(files, (file) => {
zip.file(file.name, file);
});
const blob = await zip.generateAsync(
{ type: 'blob', streamFiles: true },
(metadata) => {
peer.set('bundlingProgress', metadata.percent / 100);
},
);
return new File(
[blob],
`sharedrop-${new Date()
.toISOString()
.substring(0, 19)
.replace('T', '-')}.zip`,
{
type: 'application/zip',
},
);
},
});
================================================
FILE: app/components/popover-confirm.js
================================================
import Component from '@ember/component';
import { computed } from '@ember/object';
export default Component.extend({
classNames: ['popover-confirm'],
iconClass: computed('filename', function () {
const { filename } = this;
if (filename) {
const regex = /\.([0-9a-z]+)$/i;
const match = filename.match(regex);
const extension = match && match[1];
if (extension) {
return `glyphicon-${extension.toLowerCase()}`;
}
}
return undefined;
}),
actions: {
confirm() {
this.onConfirm();
},
cancel() {
this.onCancel();
},
},
});
================================================
FILE: app/components/room-url.js
================================================
import TextField from '@ember/component/text-field';
import $ from 'jquery';
export default TextField.extend({
classNames: ['room-url'],
didInsertElement() {
$(this.element).focus().select();
},
copyValueToClipboard() {
if (window.ClipboardEvent) {
const pasteEvent = new window.ClipboardEvent('paste', {
dataType: 'text/plain',
data: this.element.value,
});
document.dispatchEvent(pasteEvent);
}
},
});
================================================
FILE: app/components/user-widget.js
================================================
import Component from '@ember/component';
export default Component.extend({
classNames: ['peer'],
classNameBindings: ['peer.peer.state'],
});
================================================
FILE: app/controllers/application.js
================================================
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { v4 as uuidv4 } from 'uuid';
import User from '../models/user';
export default Controller.extend({
avatarService: service('avatar'),
init(...args) {
this._super(args);
const id = window.Sharedrop.userId;
const ip = window.Sharedrop.publicIp;
const avatar = this.avatarService.get();
const you = User.create({
uuid: id,
public_ip: ip,
avatarUrl: avatar.url,
label: avatar.label,
});
you.set('peer.id', id);
this.set('you', you);
},
actions: {
redirect() {
const uuid = uuidv4();
const key = `show-instructions-for-room-${uuid}`;
sessionStorage.setItem(key, 'yup');
this.transitionToRoute('room', uuid);
},
},
});
================================================
FILE: app/controllers/index.js
================================================
import Controller, { inject as controller } from '@ember/controller';
import { alias } from '@ember/object/computed';
import $ from 'jquery';
import WebRTC from '../services/web-rtc';
import Peer from '../models/peer';
export default Controller.extend({
application: controller('application'),
you: alias('application.you'),
room: null,
webrtc: null,
_onRoomConnected(event, data) {
const { you } = this;
const { room } = this;
you.get('peer').setProperties(data.peer);
// eslint-disable-next-line no-param-reassign
delete data.peer;
you.setProperties(data);
// Initialize WebRTC
this.set(
'webrtc',
new WebRTC(you.get('uuid'), {
room: room.name,
firebaseRef: window.Sharedrop.ref,
}),
);
},
_onRoomDisconnected() {
this.model.clear();
this.set('webrtc', null);
},
_onRoomUserAdded(event, data) {
const { you } = this;
if (you.get('uuid') !== data.uuid) {
this._addPeer(data);
}
},
_addPeer(attrs) {
const peerAttrs = attrs.peer;
// eslint-disable-next-line no-param-reassign
delete attrs.peer;
const peer = Peer.create(attrs);
peer.get('peer').setProperties(peerAttrs);
this.model.pushObject(peer);
},
_onRoomUserChanged(event, data) {
const peers = this.model;
const peer = peers.findBy('uuid', data.uuid);
const peerAttrs = data.peer;
const defaults = {
uuid: null,
public_ip: null,
};
if (peer) {
// eslint-disable-next-line no-param-reassign
delete data.peer;
// Firebase doesn't return keys with null values,
// so we have to add them back.
peer.setProperties($.extend({}, defaults, data));
peer.get('peer').setProperties(peerAttrs);
}
},
_onRoomUserRemoved(event, data) {
const peers = this.model;
const peer = peers.findBy('uuid', data.uuid);
peers.removeObject(peer);
},
_onPeerP2PIncomingConnection(event, data) {
const { connection } = data;
const peers = this.model;
const peer = peers.findBy('peer.id', connection.peer);
// Don't switch to 'connecting' state on incoming connection,
// as p2p connection may still fail.
peer.set('peer.connection', connection);
},
_onPeerDCIncomingConnection(event, data) {
const { connection } = data;
const peers = this.model;
const peer = peers.findBy('peer.id', connection.peer);
peer.set('peer.state', 'connected');
},
_onPeerDCIncomingConnectionError(event, data) {
const { connection, error } = data;
const peers = this.model;
const peer = peers.findBy('peer.id', connection.peer);
switch (error.type) {
case 'failed':
peer.setProperties({
'peer.connection': null,
'peer.state': 'disconnected',
state: 'error',
errorCode: 'connection-failed',
});
break;
case 'disconnected':
// TODO: notify both sides
break;
default:
break;
}
},
_onPeerP2POutgoingConnection(event, data) {
const { connection } = data;
const peers = this.model;
const peer = peers.findBy('peer.id', connection.peer);
peer.setProperties({
'peer.connection': connection,
'peer.state': 'connecting',
});
},
_onPeerDCOutgoingConnection(event, data) {
const { connection } = data;
const peers = this.model;
const peer = peers.findBy('peer.id', connection.peer);
const file = peer.get('transfer.file');
const { webrtc } = this;
const info = webrtc.getFileInfo(file);
peer.set('peer.state', 'connected');
peer.set('state', 'awaiting_response');
webrtc.sendFileInfo(connection, info);
console.log('Sending a file info...', info);
},
_onPeerDCOutgoingConnectionError(event, data) {
const { connection, error } = data;
const peers = this.model;
const peer = peers.findBy('peer.id', connection.peer);
switch (error.type) {
case 'failed':
peer.setProperties({
'peer.connection': null,
'peer.state': 'disconnected',
state: 'error',
errorCode: 'connection-failed',
});
break;
default:
break;
}
},
_onPeerP2PDisconnected(event, data) {
const { connection } = data;
const peers = this.model;
const peer = peers.findBy('peer.id', connection.peer);
if (peer) {
peer.set('peer.connection', null);
peer.set('peer.state', 'disconnected');
}
},
_onPeerP2PFileInfo(event, data) {
console.log('Peer:\t Received file info', data);
const { connection, info } = data;
const peers = this.model;
const peer = peers.findBy('peer.id', connection.peer);
peer.set('transfer.info', info);
peer.set('state', 'received_file_info');
},
_onPeerP2PFileResponse(event, data) {
console.log('Peer:\t Received file response', data);
const { connection, response } = data;
const peers = this.model;
const peer = peers.findBy('peer.id', connection.peer);
const { webrtc } = this;
if (response) {
const file = peer.get('transfer.file');
connection.on('sending_progress', (progress) => {
peer.set('transfer.sendingProgress', progress);
});
webrtc.sendFile(connection, file);
peer.set('state', 'receiving_file_data');
} else {
peer.set('state', 'declined_file_transfer');
}
},
_onPeerP2PFileCanceled(event, data) {
const { connection } = data;
const peers = this.model;
const peer = peers.findBy('peer.id', connection.peer);
connection.close();
peer.set('transfer.receivingProgress', 0);
peer.set('transfer.info', null);
peer.set('state', 'idle');
},
_onPeerP2PFileReceived(event, data) {
console.log('Peer:\t Received file', data);
const { connection } = data;
const peers = this.model;
const peer = peers.findBy('peer.id', connection.peer);
connection.close();
peer.set('transfer.receivingProgress', 0);
peer.set('transfer.info', null);
peer.set('state', 'idle');
peer.trigger('didReceiveFile');
},
_onPeerP2PFileSent(event, data) {
console.log('Peer:\t Sent file', data);
const { connection } = data;
const peers = this.model;
const peer = peers.findBy('peer.id', connection.peer);
peer.set('transfer.sendingProgress', 0);
peer.set('transfer.file', null);
peer.set('state', 'idle');
peer.trigger('didSendFile');
},
});
================================================
FILE: app/helpers/is-equal.js
================================================
import { helper as buildHelper } from '@ember/component/helper';
export default buildHelper(([leftSide, rightSide]) => leftSide === rightSide);
================================================
FILE: app/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>ShareDrop</title>
<meta
name="description"
content="ShareDrop is a peer-to-peer file sharing app powered by HTML5 WebRTC."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="mobile-web-app-capable" content="yes" />
<link
rel="shortcut icon"
sizes="196x196"
href="{{rootURL}}assets/images/sharedrop-icon.png"
/>
<link
rel="apple-touch-icon"
sizes="196x196"
href="{{rootURL}}assets/images/sharedrop-icon.png"
/>
{{content-for "head"}}
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css" />
<link
integrity=""
rel="stylesheet"
href="{{rootURL}}assets/sharedrop.css"
/>
{{content-for "head-footer"}}
</head>
<body>
<div class="preloader"><span>Loading...</span></div>
{{content-for "body"}}
<script src="https://cdn.firebase.com/js/client/2.2.9/firebase.js"></script>
<script src="{{rootURL}}assets/vendor.js"></script>
<script src="{{rootURL}}assets/sharedrop.js"></script>
{{content-for "body-footer"}}
</body>
</html>
================================================
FILE: app/initializers/prerequisites.js
================================================
/* jshint -W030 */
import $ from 'jquery';
import { Promise } from 'rsvp';
import config from 'sharedrop/config/environment';
import FileSystem from '../services/file';
import Analytics from '../services/analytics';
export function initialize(application) {
function checkWebRTCSupport() {
return new Promise((resolve, reject) => {
// window.util is a part of PeerJS library
if (window.util.supports.sctp) {
resolve();
} else {
// eslint-disable-next-line prefer-promise-reject-errors
reject('browser-unsupported');
}
});
}
function clearFileSystem() {
return new Promise((resolve, reject) => {
// TODO: change File into a service and require it here
FileSystem.removeAll()
.then(() => {
resolve();
})
.catch(() => {
// eslint-disable-next-line prefer-promise-reject-errors
reject('filesystem-unavailable');
});
});
}
function authenticateToFirebase() {
return new Promise((resolve, reject) => {
const xhr = $.getJSON('/auth');
xhr.then((data) => {
const ref = new window.Firebase(config.FIREBASE_URL);
// eslint-disable-next-line no-param-reassign
application.ref = ref;
// eslint-disable-next-line no-param-reassign
application.userId = data.id;
// eslint-disable-next-line no-param-reassign
application.publicIp = data.public_ip;
ref.authWithCustomToken(data.token, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
});
}
// TODO: move it to a separate initializer
function trackSizeOfReceivedFiles() {
$.subscribe('file_received.p2p', (event, data) => {
Analytics.trackEvent('received', {
event_category: 'file',
event_label: 'size',
value: Math.round(data.info.size / 1000),
});
});
}
application.deferReadiness();
checkWebRTCSupport()
.then(clearFileSystem)
.catch((error) => {
// eslint-disable-next-line no-param-reassign
application.error = error;
})
.then(authenticateToFirebase)
.then(trackSizeOfReceivedFiles)
.then(() => {
application.advanceReadiness();
});
}
export default {
name: 'prerequisites',
initialize,
};
================================================
FILE: app/models/peer.js
================================================
import EmberObject, { observer } from '@ember/object';
import Evented, { on } from '@ember/object/evented';
export default EmberObject.extend(Evented, {
uuid: null,
label: null,
avatarUrl: null,
public_ip: null,
peer: null,
transfer: null,
init(...args) {
this._super(args);
const initialPeerState = EmberObject.create({
id: null,
connection: null,
// State of data channel connection. Possible states:
// - disconnected
// - connecting
// - connected
state: 'disconnected',
});
const initialTransferState = EmberObject.create({
file: null,
info: null,
sendingProgress: 0,
receivingProgress: 0,
});
this.set('peer', initialPeerState);
this.set('transfer', initialTransferState);
},
// Used to display popovers. Possible states:
// - idle
// - has_selected_file
// - establishing_connection
// - awaiting_response
// - received_file_info
// - declined_file_transfer
// - receiving_file_data
// - sending_file_data
// - error
state: 'idle',
// Used to display error messages in popovers. Possible codes:
// - multiple_files
errorCode: null,
stateChanged: on(
'init',
observer('state', function () {
console.log('Peer:\t State has changed: ', this.state);
// Automatically clear error code if transitioning to a non-error state
if (this.state !== 'error') {
this.set('errorCode', null);
}
}),
),
});
================================================
FILE: app/models/user.js
================================================
import Peer from './peer';
const User = Peer.extend({
serialize() {
const data = {
uuid: this.uuid,
public_ip: this.public_ip,
label: this.label,
avatarUrl: this.avatarUrl,
peer: {
id: this.get('peer.id'),
},
};
return data;
},
});
export default User;
================================================
FILE: app/router.js
================================================
import EmberRouter from '@ember/routing/router';
import config from 'sharedrop/config/environment';
export default class Router extends EmberRouter {
location = config.locationType;
rootURL = config.rootURL;
}
// eslint-disable-next-line array-callback-return
Router.map(function () {
this.route('room', {
path: '/rooms/:room_id',
});
});
================================================
FILE: app/routes/application.js
================================================
import Route from '@ember/routing/route';
export default Route.extend({
setupController(controller) {
controller.set('currentRoute', this);
},
actions: {
openModal(modalName) {
return this.render(modalName, {
outlet: 'modal',
into: 'application',
});
},
closeModal() {
return this.disconnectOutlet({
outlet: 'modal',
parentView: 'application',
});
},
},
});
================================================
FILE: app/routes/error.js
================================================
import Route from '@ember/routing/route';
export default Route.extend({
renderTemplate(controller, error) {
const errors = ['browser-unsupported', 'filesystem-unavailable'];
const name = `errors/${error.message}`;
if (errors.indexOf(error.message) !== -1) {
this.render(name);
}
},
});
================================================
FILE: app/routes/index.js
================================================
import Route from '@ember/routing/route';
import $ from 'jquery';
import Room from '../services/room';
export default Route.extend({
beforeModel() {
const { error } = window.Sharedrop;
if (error) {
throw new Error(error);
}
},
model() {
// Get room name from the server
return $.getJSON('/room').then((data) => data.name);
},
setupController(ctrl, model) {
ctrl.set('model', []);
ctrl.set('hasCustomRoomName', false);
// Handle room events
$.subscribe('connected.room', ctrl._onRoomConnected.bind(ctrl));
$.subscribe('disconnected.room', ctrl._onRoomDisconnected.bind(ctrl));
$.subscribe('user_added.room', ctrl._onRoomUserAdded.bind(ctrl));
$.subscribe('user_changed.room', ctrl._onRoomUserChanged.bind(ctrl));
$.subscribe('user_removed.room', ctrl._onRoomUserRemoved.bind(ctrl));
// Handle peer events
$.subscribe(
'incoming_peer_connection.p2p',
ctrl._onPeerP2PIncomingConnection.bind(ctrl),
);
$.subscribe(
'incoming_dc_connection.p2p',
ctrl._onPeerDCIncomingConnection.bind(ctrl),
);
$.subscribe(
'incoming_dc_connection_error.p2p',
ctrl._onPeerDCIncomingConnectionError.bind(ctrl),
);
$.subscribe(
'outgoing_peer_connection.p2p',
ctrl._onPeerP2POutgoingConnection.bind(ctrl),
);
$.subscribe(
'outgoing_dc_connection.p2p',
ctrl._onPeerDCOutgoingConnection.bind(ctrl),
);
$.subscribe(
'outgoing_dc_connection_error.p2p',
ctrl._onPeerDCOutgoingConnectionError.bind(ctrl),
);
$.subscribe('disconnected.p2p', ctrl._onPeerP2PDisconnected.bind(ctrl));
$.subscribe('info.p2p', ctrl._onPeerP2PFileInfo.bind(ctrl));
$.subscribe('response.p2p', ctrl._onPeerP2PFileResponse.bind(ctrl));
$.subscribe('file_canceled.p2p', ctrl._onPeerP2PFileCanceled.bind(ctrl));
$.subscribe('file_received.p2p', ctrl._onPeerP2PFileReceived.bind(ctrl));
$.subscribe('file_sent.p2p', ctrl._onPeerP2PFileSent.bind(ctrl));
// Join the room
const room = new Room(model, window.Sharedrop.ref);
room.join(ctrl.get('you').serialize());
ctrl.set('room', room);
},
renderTemplate() {
this.render();
this.render('about_you', {
into: 'application',
outlet: 'about_you',
});
const key = 'show-instructions-for-app';
if (!localStorage.getItem(key)) {
this.send('openModal', 'about_app');
localStorage.setItem(key, 'yup');
}
},
actions: {
willTransition() {
$.unsubscribe('.room');
$.unsubscribe('.p2p');
// eslint-disable-next-line ember/no-controller-access-in-routes
const controller = this.controllerFor('index');
controller.get('room').leave();
return true;
},
},
});
================================================
FILE: app/routes/room.js
================================================
import IndexRoute from './index';
export default IndexRoute.extend({
controllerName: 'index',
model(params) {
// Get room name from params
return params.room_id;
},
afterModel(model, transition) {
transition.then((route) => {
route
.controllerFor('application')
.set('currentUrl', window.location.href);
});
},
setupController(ctrl, model) {
// Call this method on "index" controller
this._super(ctrl, model);
ctrl.set('hasCustomRoomName', true);
},
renderTemplate(ctrl) {
this.render('index');
this.render('about_you', {
into: 'application',
outlet: 'about_you',
});
const room = ctrl.get('room').name;
const key = `show-instructions-for-room-${room}`;
if (sessionStorage.getItem(key)) {
this.send('openModal', 'about_room');
sessionStorage.removeItem(key);
}
},
});
================================================
FILE: app/services/analytics.js
================================================
export default {
trackEvent(name, parameters) {
if (window.gtag && typeof window.gtag === 'function') {
window.gtag('event', name, parameters);
}
},
};
================================================
FILE: app/services/avatar.js
================================================
import Service from '@ember/service';
import sample from 'lodash/sample';
import startCase from 'lodash/startCase';
const AVATARS = [
{
name: 'Piglet',
id: '23',
},
{
name: 'Cat',
id: '36',
},
{
name: 'Fish',
id: '37',
},
{
name: 'Fox',
id: '38',
},
{
name: 'Chicken',
id: '46',
},
{
name: 'Goat',
id: '50',
},
{
name: 'Ram',
id: '51',
},
{
name: 'Sheep',
id: '52',
},
{
name: 'Bison',
id: '59',
},
{
name: 'Dog',
id: '61',
},
{
name: 'Walrus',
id: '62',
},
{
name: 'Dog',
id: '63',
},
{
name: 'Monkey',
id: '64',
},
{
name: 'Bear',
id: '65',
},
{
name: 'Lion',
id: '66',
},
{
name: 'Zebra',
id: '67',
},
{
name: 'Giraffe',
id: '68',
},
{
name: 'Bear',
id: '71',
},
{
name: 'Wolf',
id: '74',
},
{
name: 'Rhino',
id: '86',
},
{
name: 'Bat',
id: '87',
},
{
name: 'Cat',
id: '95',
},
{
name: 'Penguin',
id: '102',
},
{
name: 'Rhino',
id: '109',
},
{
name: 'Koala',
id: '112',
},
];
const PREFIXES = [
'adventurous',
'affable',
'ambitious',
'amiable ',
'amusing',
'brave',
'bright',
'charming',
'compassionate',
'convivial',
'courageous',
'creative',
'diligent',
'easygoing',
'emotional',
'energetic',
'enthusiastic',
'exuberant',
'fearless',
'friendly',
'funny',
'generous',
'gentle',
'good',
'helpful',
'honest',
'humorous',
'imaginative',
'independent',
'intelligent',
'intuitive',
'inventive',
'kind',
'loving',
'loyal',
'modest',
'neat',
'nice',
'optimistic',
'passionate',
'patient',
'persistent',
'polite',
'practical',
'rational',
'reliable',
'reserved',
'resourceful',
'romantic',
'sensible',
'sensitive',
'sincere',
'sympathetic',
'thoughtful',
'tough',
'understanding',
'versatile',
'warmhearted',
];
const Avatar = Service.extend({
get() {
const avatar = sample(AVATARS);
const prefix = sample(PREFIXES);
return {
url: `/assets/images/avatars/${avatar.id}.svg`,
label: startCase(`${prefix} ${avatar.name}`),
};
},
});
export default Avatar;
================================================
FILE: app/services/file.js
================================================
import { Promise } from 'rsvp';
const File = function (options) {
const self = this;
this.name = options.name;
this.localName = `${new Date().getTime()}-${this.name}`;
this.size = options.size;
this.type = options.type;
this._reset();
return new Promise((resolve, reject) => {
const requestFileSystem =
window.requestFileSystem || window.webkitRequestFileSystem;
requestFileSystem(
window.TEMPORARY,
options.size,
(filesystem) => {
self.filesystem = filesystem;
resolve(self);
},
(error) => {
self.errorHandler(error);
reject(error);
},
);
});
};
File.removeAll = function () {
return new Promise((resolve, reject) => {
const filer = new window.Filer();
filer.init(
{ persistent: false },
() => {
filer.ls('/', (entries) => {
function rm(entry) {
if (entry) {
filer.rm(entry, () => {
rm(entries.pop());
});
} else {
resolve();
}
}
rm(entries.pop());
});
},
(error) => {
console.log(error);
reject(error);
},
);
});
};
File.prototype.append = function (data) {
const self = this;
const options = {
create: this.create,
};
return new Promise((resolve, reject) => {
self.filesystem.root.getFile(
self.localName,
options,
(fileEntry) => {
if (self.create) {
self.create = false;
}
self.fileEntry = fileEntry;
fileEntry.createWriter(
(writer) => {
const blob = new Blob(data, { type: self.type });
// console.log('File: Appending ' + blob.size + ' bytes at ' + self.seek);
// eslint-disable-next-line no-param-reassign
writer.onwriteend = function () {
self.seek += blob.size;
resolve(fileEntry);
};
// eslint-disable-next-line no-param-reassign
writer.onerror = function (error) {
self.errorHandler(error);
reject(error);
};
writer.seek(self.seek);
writer.write(blob);
},
(error) => {
self.errorHandler(error);
reject(error);
},
);
},
(error) => {
self.errorHandler(error);
reject(error);
},
);
});
};
File.prototype.save = function () {
const self = this;
console.log('File: Saving file: ', this.fileEntry);
const a = document.createElement('a');
a.download = this.name;
function finish(link) {
document.body.appendChild(a);
link.addEventListener('click', () => {
// Remove file entry from filesystem.
setTimeout(() => {
self.remove().then(self._reset.bind(self));
}, 100); // Hack, but otherwise browser doesn't save the file at all.
link.parentNode.removeChild(a);
});
link.click();
}
if (this._isWebKit()) {
a.href = this.fileEntry.toURL();
finish(a);
} else {
this.fileEntry.file((file) => {
const URL = window.URL || window.webkitURL;
a.href = URL.createObjectURL(file);
finish(a);
});
}
};
File.prototype.errorHandler = function (error) {
console.error('File error: ', error);
};
File.prototype.remove = function () {
const self = this;
return new Promise((resolve, reject) => {
self.filesystem.root.getFile(
self.localName,
{ create: false },
(fileEntry) => {
fileEntry.remove(
() => {
console.debug(`File: Removed file: ${self.localName}`);
resolve(fileEntry);
},
(error) => {
self.errorHandler(error);
reject(error);
},
);
},
(error) => {
self.errorHandler(error);
reject(error);
},
);
});
};
File.prototype._reset = function () {
this.create = true;
this.filesystem = null;
this.fileEntry = null;
this.seek = 0;
};
File.prototype._isWebKit = function () {
return !!window.webkitRequestFileSystem;
};
export default File;
================================================
FILE: app/services/room.js
================================================
import $ from 'jquery';
// TODO: use Ember.Object.extend()
const Room = function (name, firebaseRef) {
this._ref = firebaseRef;
this.name = name;
};
Room.prototype.join = function (user) {
const self = this;
// Setup Firebase refs
self._connectionRef = self._ref.child('.info/connected');
self._roomRef = self._ref.child(`rooms/${this.name}`);
self._usersRef = self._roomRef.child('users');
self._userRef = self._usersRef.child(user.uuid);
console.log('Room:\t Connecting to: ', this.name);
self._connectionRef.on('value', (connectionSnapshot) => {
// Once connected (or reconnected) to Firebase
if (connectionSnapshot.val() === true) {
console.log('Firebase: (Re)Connected');
// Remove yourself from the room when disconnected
self._userRef.onDisconnect().remove();
// Join the room
self._userRef.set(user, (error) => {
if (error) {
console.warn('Firebase: Adding user to the room failed: ', error);
} else {
console.log('Firebase: User added to the room');
// Create a copy of user data,
// so that deleting properties won't affect the original variable
$.publish('connected.room', $.extend(true, {}, user));
}
});
self._usersRef.on('child_added', (userAddedSnapshot) => {
const addedUser = userAddedSnapshot.val();
console.log('Room:\t user_added: ', addedUser);
$.publish('user_added.room', addedUser);
});
self._usersRef.on(
'child_removed',
(userRemovedSnapshot) => {
const removedUser = userRemovedSnapshot.val();
console.log('Room:\t user_removed: ', removedUser);
$.publish('user_removed.room', removedUser);
},
() => {
// Handle case when the whole room is removed from Firebase
$.publish('disconnected.room');
},
);
self._usersRef.on('child_changed', (userChangedSnapshot) => {
const changedUser = userChangedSnapshot.val();
console.log('Room:\t user_changed: ', changedUser);
$.publish('user_changed.room', changedUser);
});
} else {
console.log('Firebase: Disconnected');
$.publish('disconnected.room');
self._usersRef.off();
}
});
return this;
};
Room.prototype.update = function (attrs) {
this._userRef.update(attrs);
};
Room.prototype.leave = function () {
this._userRef.remove();
this._usersRef.off();
};
export default Room;
================================================
FILE: app/services/web-rtc.js
================================================
// TODO:
// - provide TURN server config once it's possible to create rooms with custom names
// - use Ember.Object.extend()
import $ from 'jquery';
import File from './file';
const WebRTC = function (id, options) {
const defaults = {
config: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] },
debug: 3,
};
this.conn = new window.Peer(id, $.extend(defaults, options));
this.files = {
outgoing: {},
incoming: {},
};
// Listen for incoming connections
this.conn.on('connection', (connection) => {
$.publish('incoming_peer_connection.p2p', { connection });
connection.on('open', () => {
console.log('Peer:\t Data channel connection opened: ', connection);
$.publish('incoming_dc_connection.p2p', { connection });
});
connection.on('error', (error) => {
console.log('Peer:\t Data channel connection error', error);
$.publish('incoming_dc_connection_error.p2p', {
connection,
error,
});
});
this._onConnection(connection);
});
this.conn.on('close', () => {
console.log('Peer:\t Connection to server closed.');
});
this.conn.on('error', (error) => {
console.log('Peer:\t Error while connecting to server: ', error);
});
};
WebRTC.CHUNK_MTU = 16000;
WebRTC.CHUNKS_PER_ACK = 64;
WebRTC.prototype.connect = function (id) {
const connection = this.conn.connect(id, {
label: 'file',
reliable: true,
serialization: 'none', // we handle serialization ourselves
});
connection.on('open', () => {
console.log('Peer:\t Data channel connection opened: ', connection);
$.publish('outgoing_dc_connection.p2p', { connection });
});
connection.on('error', (error) => {
console.log('Peer:\t Data channel connection error', error);
$.publish('outgoing_dc_connection_error.p2p', {
connection,
error,
});
});
$.publish('outgoing_peer_connection.p2p', { connection });
this._onConnection(connection);
};
WebRTC.prototype._onConnection = function (connection) {
const self = this;
console.log('Peer:\t Opening data channel connection...', connection);
connection.on('data', (data) => {
// Lame type check
if (data.byteLength !== undefined) {
// ArrayBuffer
self._onBinaryData(data, connection);
} else {
// JSON string
self._onJSONData(JSON.parse(data), connection);
}
});
connection.on('close', () => {
$.publish('disconnected.p2p', { connection });
console.log('Peer:\t P2P connection closed: ', connection);
});
};
WebRTC.prototype._onBinaryData = function (data, connection) {
const self = this;
const incoming = this.files.incoming[connection.peer];
const { info, file, block, receivedChunkNum } = incoming;
const chunksPerAck = WebRTC.CHUNKS_PER_ACK;
// TODO move it after requesting a new block to speed things up
connection.emit(
'receiving_progress',
(receivedChunkNum + 1) / info.chunksTotal,
);
// console.log('Got chunk no ' + (receivedChunkNum + 1) + ' out of ' + info.chunksTotal);
block.push(data);
incoming.receivedChunkNum = receivedChunkNum + 1;
const nextChunkNum = incoming.receivedChunkNum;
const lastChunkInFile = receivedChunkNum === info.chunksTotal - 1;
const lastChunkInBlock =
receivedChunkNum > 0 && (receivedChunkNum + 1) % chunksPerAck === 0;
if (lastChunkInFile || lastChunkInBlock) {
file.append(block).then(() => {
if (lastChunkInFile) {
file.save();
$.publish('file_received.p2p', {
blob: file,
info,
connection,
});
} else {
// console.log('Requesting block starting at: ' + (nextChunkNum));
incoming.block = [];
self._requestFileBlock(connection, nextChunkNum);
}
});
}
};
WebRTC.prototype._onJSONData = function (data, connection) {
switch (data.type) {
case 'info': {
const info = data.payload;
$.publish('info.p2p', {
connection,
info,
});
// Store incoming file info for later
this.files.incoming[connection.peer] = {
info,
file: null,
block: [],
receivedChunkNum: 0,
};
console.log('Peer:\t File info: ', data);
break;
}
case 'cancel': {
$.publish('file_canceled.p2p', {
connection,
});
console.log('Peer:\t Sender canceled file transfer');
break;
}
case 'response': {
const response = data.payload;
// If recipient rejected the file, delete stored file
if (!response) {
delete this.files.outgoing[connection.peer];
}
$.publish('response.p2p', {
connection,
response,
});
console.log('Peer:\t File response: ', data);
break;
}
case 'block_request': {
const { file } = this.files.outgoing[connection.peer];
// console.log('Peer:\t Block request: ', data.payload);
this._sendBlock(connection, file, data.payload);
break;
}
default:
console.log('Peer:\t Unknown message: ', data);
}
};
WebRTC.prototype.getFileInfo = function (file) {
return {
lastModifiedDate: file.lastModifiedDate,
name: file.name,
size: file.size,
type: file.type,
chunksTotal: Math.ceil(file.size / WebRTC.CHUNK_MTU),
};
};
WebRTC.prototype.sendFileInfo = function (connection, info) {
const message = {
type: 'info',
payload: info,
};
connection.send(JSON.stringify(message));
};
WebRTC.prototype.sendCancelRequest = function (connection) {
const message = {
type: 'cancel',
};
connection.send(JSON.stringify(message));
};
WebRTC.prototype.sendFileResponse = function (connection, response) {
const message = {
type: 'response',
payload: response,
};
if (response) {
// If recipient accepted the file, request required space to store the file on HTML5 filesystem
const incoming = this.files.incoming[connection.peer];
const { info } = incoming;
new File({ name: info.name, size: info.size, type: info.type }).then(
(file) => {
incoming.file = file;
connection.send(JSON.stringify(message));
},
);
} else {
// Otherwise, delete stored file info
delete this.files.incoming[connection.peer];
connection.send(JSON.stringify(message));
}
};
WebRTC.prototype.sendFile = function (connection, file) {
// Save the file for later
this.files.outgoing[connection.peer] = {
file,
info: this.getFileInfo(file),
};
// Send the first block. Next ones will be requested by recipient.
this._sendBlock(connection, file, 0);
};
WebRTC.prototype._requestFileBlock = function (connection, chunkNum) {
const message = {
type: 'block_request',
payload: chunkNum,
};
connection.send(JSON.stringify(message));
};
WebRTC.prototype._sendBlock = function (connection, file, beginChunkNum) {
const { info } = this.files.outgoing[connection.peer];
const chunkSize = WebRTC.CHUNK_MTU;
const chunksPerAck = WebRTC.CHUNKS_PER_ACK;
const remainingChunks = info.chunksTotal - beginChunkNum;
const chunksToSend = Math.min(remainingChunks, chunksPerAck);
const endChunkNum = beginChunkNum + chunksToSend - 1;
const blockBegin = beginChunkNum * chunkSize;
const blockEnd = endChunkNum * chunkSize + chunkSize;
const reader = new FileReader();
let chunkNum;
// Read the whole block from file
const blockBlob = file.slice(blockBegin, blockEnd);
// console.log('Sending block: start chunk: ' + beginChunkNum + ' end chunk: ' + endChunkNum);
// console.log('Sending block: start byte : ' + begin + ' end byte : ' + end);
reader.onload = function (event) {
if (reader.readyState === FileReader.DONE) {
const blockBuffer = event.target.result;
for (
chunkNum = beginChunkNum;
chunkNum < endChunkNum + 1;
chunkNum += 1
) {
// Send each chunk (begin index is inclusive, end index is exclusive)
const bufferBegin = (chunkNum % chunksPerAck) * chunkSize;
const bufferEnd = Math.min(
bufferBegin + chunkSize,
blockBuffer.byteLength,
);
const chunkBuffer = blockBuffer.slice(bufferBegin, bufferEnd);
connection.send(chunkBuffer);
// console.log('Sent chunk: start byte: ' + begin + ' end byte: ' + end + ' length: ' + chunkBuffer.byteLength);
// console.log('Sent chunk no ' + (chunkNum + 1) + ' out of ' + info.chunksTotal);
connection.emit('sending_progress', (chunkNum + 1) / info.chunksTotal);
}
if (endChunkNum === info.chunksTotal - 1) {
$.publish('file_sent.p2p', { connection });
}
}
};
reader.readAsArrayBuffer(blockBlob);
};
export default WebRTC;
================================================
FILE: app/styles/app.sass
================================================
@import base/reset
@import base/variables
@import base/mixins
@import base/element_defaults
@import base/glyphicons_filetypes
@import modules/modules
@import modules/modal
@import modules/users
@import modules/popover
@import layout/header
@import layout/content
@import layout/footer
@import layout/media
================================================
FILE: app/styles/base/_element_defaults.sass
================================================
*, *::before, *::after
box-sizing: border-box
html
height: 100%
font-family: $font-family
font-size: 10px
body
height: 100%
a
text-decoration: none
b, strong
font-weight: bold
input, select
font-family: inherit
padding: 1rem
border: 1px solid #ccc
width: 100%
border-radius: .3rem
font-size: 1.4rem
.invisible
height: 0
width: 0
opacity: 0
================================================
FILE: app/styles/base/_glyphicons_filetypes.sass
================================================
/* GLYPHICONS FILETYPES 1.8 */
@font-face
font-family: "Glyphicons Filetypes"
src: url(/assets/fonts/glyphicons/glyphicons-filetypes-regular.woff) format("woff"), url(/assets/fonts/glyphicons/glyphicons-filetypes-regular.ttf) format("truetype"), url(/assets/fonts/glyphicons/glyphicons-filetypes-regular.svg#glyphicons_filetypesregular) format("svg")
$icon-prefix: glyphicon
[class*="#{$icon-prefix}-"]
display: inline-block
vertical-align: middle
font:
family: "Glyphicons Filetypes"
style: normal
weight: normal
size: 1em
.#{$icon-prefix}-spin
animation: #{$icon-prefix}-spin 2s infinite linear
@-moz-keyframes #{$icon-prefix}-spin
0%
-moz-transform: rotate(0deg)
100%
-moz-transform: rotate(359deg)
@-webkit-keyframes #{$icon-prefix}-spin
0%
-webkit-transform: rotate(0deg)
100%
-webkit-transform: rotate(359deg)
@-o-keyframes #{$icon-prefix}-spin
0%
-o-transform: rotate(0deg)
100%
-o-transform: rotate(359deg)
@-ms-keyframes #{$icon-prefix}-spin
0%
-ms-transform: rotate(0deg)
100%
-ms-transform: rotate(359deg)
@keyframes #{$icon-prefix}-spin
0%
transform: rotate(0deg)
100%
transform: rotate(359deg)
[class*="#{$icon-prefix}-"]:before
content: "\E120"
.#{$icon-prefix}-txt:before
content: "\E001"
.#{$icon-prefix}-doc:before
content: "\E002"
.#{$icon-prefix}-rtf:before
content: "\E003"
.#{$icon-prefix}-log:before
content: "\E004"
.#{$icon-prefix}-tex:before
content: "\E005"
.#{$icon-prefix}-msg:before
content: "\E006"
.#{$icon-prefix}-text:before
content: "\E007"
.#{$icon-prefix}-wpd:before
content: "\E008"
.#{$icon-prefix}-wps:before
content: "\E009"
.#{$icon-prefix}-docx:before
content: "\E010"
.#{$icon-prefix}-page:before
content: "\E011"
.#{$icon-prefix}-csv:before
content: "\E012"
.#{$icon-prefix}-dat:before
content: "\E013"
.#{$icon-prefix}-tar:before
content: "\E014"
.#{$icon-prefix}-xml:before
content: "\E015"
.#{$icon-prefix}-vcf:before
content: "\E016"
.#{$icon-prefix}-pps:before
content: "\E017"
.#{$icon-prefix}-key:before
content: "\E018"
.#{$icon-prefix}-ppt:before
content: "\E019"
.#{$icon-prefix}-pptx:before
content: "\E020"
.#{$icon-prefix}-sdf:before
content: "\E021"
.#{$icon-prefix}-gbr:before
content: "\E022"
.#{$icon-prefix}-ged:before
content: "\E023"
.#{$icon-prefix}-mp3:before
content: "\E024"
.#{$icon-prefix}-m4a:before
content: "\E025"
.#{$icon-prefix}-waw:before
content: "\E026"
.#{$icon-prefix}-wma:before
content: "\E027"
.#{$icon-prefix}-mpa:before
content: "\E028"
.#{$icon-prefix}-iff:before
content: "\E029"
.#{$icon-prefix}-aif:before
content: "\E030"
.#{$icon-prefix}-ra:before
content: "\E031"
.#{$icon-prefix}-mid:before
content: "\E032"
.#{$icon-prefix}-m3v:before
content: "\E033"
.#{$icon-prefix}-e_3gp:before
content: "\E034"
.#{$icon-prefix}-shf:before
content: "\E035"
.#{$icon-prefix}-avi:before
content: "\E036"
.#{$icon-prefix}-asx:before
content: "\E037"
.#{$icon-prefix}-mp4:before
content: "\E038"
.#{$icon-prefix}-e_3g2:before
content: "\E039"
.#{$icon-prefix}-mpg:before
content: "\E040"
.#{$icon-prefix}-asf:before
content: "\E041"
.#{$icon-prefix}-vob:before
content: "\E042"
.#{$icon-prefix}-wmv:before
content: "\E043"
.#{$icon-prefix}-mov:before
content: "\E044"
.#{$icon-prefix}-srt:before
content: "\E045"
.#{$icon-prefix}-m4v:before
content: "\E046"
.#{$icon-prefix}-flv:before
content: "\E047"
.#{$icon-prefix}-rm:before
content: "\E048"
.#{$icon-prefix}-png:before
content: "\E049"
.#{$icon-prefix}-psd:before
content: "\E050"
.#{$icon-prefix}-psp:before
content: "\E051"
.#{$icon-prefix}-jpg:before, .#{$icon-prefix}-jpeg:before
content: "\E052"
.#{$icon-prefix}-tif:before
content: "\E053"
.#{$icon-prefix}-tiff:before
content: "\E054"
.#{$icon-prefix}-gif:before
content: "\E055"
.#{$icon-prefix}-bmp:before
content: "\E056"
.#{$icon-prefix}-tga:before
content: "\E057"
.#{$icon-prefix}-thm:before
content: "\E058"
.#{$icon-prefix}-yuv:before
content: "\E059"
.#{$icon-prefix}-dds:before
content: "\E060"
.#{$icon-prefix}-ai:before
content: "\E061"
.#{$icon-prefix}-eps:before
content: "\E062"
.#{$icon-prefix}-ps:before
content: "\E063"
.#{$icon-prefix}-svg:before
content: "\E064"
.#{$icon-prefix}-pdf:before
content: "\E065"
.#{$icon-prefix}-pct:before
content: "\E066"
.#{$icon-prefix}-indd:before
content: "\E067"
.#{$icon-prefix}-xlr:before
content: "\E068"
.#{$icon-prefix}-xls:before
content: "\E069"
.#{$icon-prefix}-xlsx:before
content: "\E070"
.#{$icon-prefix}-db:before
content: "\E071"
.#{$icon-prefix}-dbf:before
content: "\E072"
.#{$icon-prefix}-mdb:before
content: "\E073"
.#{$icon-prefix}-pdb:before
content: "\E074"
.#{$icon-prefix}-sql:before
content: "\E075"
.#{$icon-prefix}-aacd:before
content: "\E076"
.#{$icon-prefix}-app:before
content: "\E077"
.#{$icon-prefix}-exe:before
content: "\E078"
.#{$icon-prefix}-com:before
content: "\E079"
.#{$icon-prefix}-bat:before
content: "\E080"
.#{$icon-prefix}-apk:before
content: "\E081"
.#{$icon-prefix}-jar:before
content: "\E082"
.#{$icon-prefix}-hsf:before
content: "\E083"
.#{$icon-prefix}-pif:before
content: "\E084"
.#{$icon-prefix}-vb:before
content: "\E085"
.#{$icon-prefix}-cgi:before
content: "\E086"
.#{$icon-prefix}-css:before
content: "\E087"
.#{$icon-prefix}-js:before
content: "\E088"
.#{$icon-prefix}-php:before
content: "\E089"
.#{$icon-prefix}-xhtml:before
content: "\E090"
.#{$icon-prefix}-htm:before
content: "\E091"
.#{$icon-prefix}-html:before
content: "\E092"
.#{$icon-prefix}-asp:before
content: "\E093"
.#{$icon-prefix}-cer:before
content: "\E094"
.#{$icon-prefix}-jsp:before
content: "\E095"
.#{$icon-prefix}-cfm:before
content: "\E096"
.#{$icon-prefix}-aspx:before
content: "\E097"
.#{$icon-prefix}-rss:before
content: "\E098"
.#{$icon-prefix}-csr:before
content: "\E099"
.#{$icon-prefix}-less:before
content: "\003C"
.#{$icon-prefix}-otf:before
content: "\E101"
.#{$icon-prefix}-ttf:before
content: "\E102"
.#{$icon-prefix}-font:before
content: "\E103"
.#{$icon-prefix}-fnt:before
content: "\E104"
.#{$icon-prefix}-eot:before
content: "\E105"
.#{$icon-prefix}-woff:before
content: "\E106"
.#{$icon-prefix}-zip:before
content: "\E107"
.#{$icon-prefix}-zipx:before
content: "\E108"
.#{$icon-prefix}-rar:before
content: "\E109"
.#{$icon-prefix}-targ:before
content: "\E110"
.#{$icon-prefix}-sitx:before
content: "\E111"
.#{$icon-prefix}-deb:before
content: "\E112"
.#{$icon-prefix}-e_7z:before
content: "\E113"
.#{$icon-prefix}-pkg:before
content: "\E114"
.#{$icon-prefix}-rpm:before
content: "\E115"
.#{$icon-prefix}-cbr:before
content: "\E116"
.#{$icon-prefix}-gz:before
content: "\E117"
.#{$icon-prefix}-dmg:before
content: "\E118"
.#{$icon-prefix}-cue:before
content: "\E119"
.#{$icon-prefix}-bin:before
content: "\E120"
.#{$icon-prefix}-iso:before
content: "\E121"
.#{$icon-prefix}-hdf:before
content: "\E122"
.#{$icon-prefix}-vcd:before
content: "\E123"
.#{$icon-prefix}-bak:before
content: "\E124"
.#{$icon-prefix}-tmp:before
content: "\E125"
.#{$icon-prefix}-ics:before
content: "\E126"
.#{$icon-prefix}-msi:before
content: "\E127"
.#{$icon-prefix}-cfg:before
content: "\E128"
.#{$icon-prefix}-ini:before
content: "\E129"
.#{$icon-prefix}-prf:before
content: "\E130"
================================================
FILE: app/styles/base/_mixins.sass
================================================
=ellipsis
overflow: hidden
white-space: nowrap
text-overflow: ellipsis
=button_reset
margin: 0
padding: 0
display: inline-block
border: none
background: none
outline: none
width: auto
cursor: pointer
font-family: inherit
=shape($size, $shape)
display: inline-block
width: $size
height: $size
line-height: $size
text-align: center
vertical-align: middle
@if $shape == circle
border-radius: 50%
================================================
FILE: app/styles/base/_reset.sass
================================================
a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video
margin: 0
padding: 0
border: 0
font: inherit
font-size: 100%
vertical-align: baseline
html
line-height: 1
ol,ul
list-style: none
table
border-collapse: collapse
border-spacing: 0
caption,td,th
text-align: left
font-weight: 400
vertical-align: middle
blockquote,q
quotes: none
blockquote:after,blockquote:before,q:after,q:before
content: ""
content: none
a img
border: 0
article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary
display: block
================================================
FILE: app/styles/base/_variables.sass
================================================
$font-family: "Helvetica Neue", sans-serif
$blue: #0088cc
$green: #a4c540
================================================
FILE: app/styles/layout/_content.sass
================================================
.l-content
position: relative
height: 100vh
min-height: 600px
.visually-hidden
clip: rect(0 0 0 0)
clip-path: inset(50%)
height: 1px
overflow: hidden
position: absolute
white-space: nowrap
width: 1px
.ribbon
position: fixed
left: -80px
bottom: 0px
width: 300px
height: 64px
transform: rotate(45deg)
z-index: 999
background: linear-gradient(-180deg, rgb(0, 91, 187) 50%, rgb(255, 213, 0) 50%)
opacity: 0.8
================================================
FILE: app/styles/layout/_footer.sass
================================================
.l-footer
position: fixed
z-index: 200
bottom: 0
left: 0
right: 0
background-color: rgba(white,.6)
text-align: center
color: #b0b0b0
a
opacity: .6
transition: opacity .2s linear
&:hover
opacity: 1
> span
display: none
.about
display: inline-block
font-size: 1.1rem
line-height: 1.4
margin-top: .2em
padding: 0 10px
.logos
display: flex
align-items: center
justify-content: space-between
padding: 10px
.left,
.right
display: flex
.github
width: 20px
height: 20px
background: transparent url("../assets/images/github.svg") no-repeat center
.twitter
width: 80px
height: 20px
margin-left: 8px
.donate
img
height: 20px
================================================
FILE: app/styles/layout/_header.sass
================================================
.l-header
.navbar
position: fixed
z-index: 10
top: 0
left: 0
width: 100%
height: 60px
background: transparent
color: black
user-select: none
.logo
position: relative
height: 38px
width: 162px
margin: 15px 0 0 15px
background: transparent url("../assets/images/sharedrop.svg") no-repeat left top
.logo-title
display: none
.logo-subtitle
position: absolute
bottom: 0
left: 44px
font-size: 1rem
font-weight: bold
img
vertical-align: top
.nav
position: absolute
top: 15px
right: 15px
> li
float: left
margin-left: 15px
font-size: 1.4rem
line-height: 30px
a
display: inline-block
transition: opacity .175s linear
color: black
img
vertical-align: middle
&:hover
opacity: .6
.icon-create-room
font-size: 28px
line-height: 24px
.icon-help
+shape(18px, circle)
border: 1px solid black
font-size: 1.2rem
opacity: .18
margin-top: -2px
================================================
FILE: app/styles/layout/_media.sass
================================================
@media (max-height: 520px)
.modal-body
height: auto
bottom: auto
margin: 15px auto
@media (max-width: 768px)
.preloader
width: 240px
height: 41px
background-size: 240px 41px
.modal-body
width: 90%
height: auto
bottom: auto
margin: 15px auto
.note
display: block
.l-content
padding: 80px 0 115px
min-height: inherit
height: auto
.user
.peer
position: relative
left: auto !important
bottom: auto !important
height: 106px
width: 100%
padding: 15px
border-bottom: 1px solid #eee
margin: 0 !important
text-align: left
.avatar
z-index: 2
.user-info
z-index: 1
position: absolute
top: 30px
left: 0
padding-left: 170px
width: 100%
.user-email
font-size: 1.4rem
.user-label
font-size: 1.8rem
.user-ip
font-size: 1.2rem
color: #808080
.user-connection-status
top: 0
left: -9.2rem
margin-top: 0
transform: scale(1.75, 1.75)
&.you
.peer
border-bottom: none
.l-header
.navbar
z-index: 200
background: rgba(white,.7)
.email
display: none
.nav
> li
margin-left: 10px
.l-footer
padding-top: 10px
.circles
display: none
.error
width: auto
font-size: 1.4rem
padding: 0 15px
================================================
FILE: app/styles/modules/_modal.sass
================================================
.modal-overlay
position: fixed
z-index: 300
top: 0
bottom: 0
left: 0
right: 0
background-color: rgba(black,.6)
overflow: auto
.modal-body
position: absolute
z-index: 301
top: 10%
left: 0
right: 0
margin: auto
background-color: white
width: 580px
padding: 20px
margin-bottom: 40px
border-radius: 5px
h3
font-size: 1.8rem
font-weight: bold
margin-bottom: 0.5em
h4
font-size: 1.5rem
font-weight: bold
margin-bottom: 0.5em
p
font-size: 1.4rem
line-height: 1.4em
margin-bottom: 1.42em
&.note
font-size: 1.1rem
a
color: $blue
opacity: .6
&:hover
opacity: 1
.qr-code
div
padding: 15px
img
margin-left: auto
margin-right: auto
.actions
text-align: center
button
+button_reset
font-size: 1.6rem
cursor: pointer
border-radius: 5px
background: rgba($blue,.8)
color: #fff
padding: 14px 80px
text-shadow: rgba(black,.3) 0 -1px 0
transition: background .3s
margin-bottom: 20px
&:hover
background: $blue
.logo
height: 38px
margin-bottom: 20px
background: transparent url("../assets/images/sharedrop.svg") no-repeat left
span
display: none
.plus-icon
font-weight: bold
font-size: 2rem
================================================
FILE: app/styles/modules/_modules.sass
================================================
.preloader
position: absolute
left: 0
right: 0
top: 0
bottom: 0
margin: auto
width: 324px
height: 56px
background: transparent url("../assets/images/sharedrop.svg") no-repeat center
background-size: 324px 56px
transition: opacity .2s
> span
position: absolute
text-align: center
width: 100%
bottom: -18px
font-size: 1.4rem
.ember-application
.preloader
opacity: 0
.error
position: absolute
top: 0
bottom: 0
left: 0
right: 0
margin: auto
width: 50rem
height: 15rem
text-align: center
font-size: 1.8rem
line-height: 1.5em
.circles
position: absolute
bottom: 0
left: 50%
width: 1140px
margin-left: -570px
height: 700px
z-index: -1
transform-origin: 570px 570px
animation: grow 1.5s ease-out
.circle
stroke-width: .4
fill: rgba(0,0,0,0)
@keyframes grow
50%
transform: scale(1.5, 1.5)
opacity: .2
51%
transform: scale(0.5, 0.5)
opacity: 0
================================================
FILE: app/styles/modules/_popover.sass
================================================
$popover-border-color: #c0c0c0
.popover
position: absolute
bottom: 100%
left: 50%
transform: translateX(-50%)
z-index: 10
background-color: #fff
border: 1px solid $popover-border-color
padding: 10px
border-radius: 5px
width: 360px
box-shadow: rgba(black,.3) 0 1px 3px
text-align: left
margin-bottom: 5px
&::after
position: absolute
bottom: 0
left: 50%
margin: 0 0 -5px -5px
content: ''
width: 10px
height: 10px
background: inherit
transform: rotate(45deg)
border: 1px solid transparent
border-right-color: $popover-border-color
border-bottom-color: $popover-border-color
.popover-body
position: relative
padding-left: 60px
p
word-break: break-all
overflow: hidden
font-size: 12px
line-height: 1.4em
margin-bottom: 1em
min-height: 28px
.popover-icon
position: absolute
left: 0
top: 0
font-size: 50px
.popover-buttons
text-align: right
@media (max-width: 768px)
.popover
left: 0
right: 0
top: 0
bottom: 0
border: none
width: auto
box-shadow: none
border-radius: 0
margin: 0
transform: none
background-color: rgba(#f0f0f0, 0.9)
&::after
display: none
.popover-buttons
button
font-size: 18px
================================================
FILE: app/styles/modules/_users.sass
================================================
$user-size: 76px
.user
user-select: none
.peer
position: absolute
left: 50%
bottom: 300px
width: $user-size
height: $user-size
margin-left: -$user-size / 2
text-align: center
.avatar
position: relative
width: $user-size
height: $user-size
transition: all .2s ease-in-out
svg
top: 0
bottom: 0
z-index: -1
.gravatar
position: absolute
top: 5px
left: 5px
z-index: 1
border: 1px solid #c0c0c0
box-shadow: rgba(black, 0.2) 0 0 3px
width: $user-size - 10
height: $user-size - 10
border-radius: 50%
animation: shadow .8s ease-in
transition: all .2s ease-in-out
.user-info
position: absolute
top: $user-size
left: 50%
width: 140px
margin-left: -70px
.user-label, .user-email
font-weight: bold
color: #606060
padding-bottom: .4rem
.user-email
font-size: 1rem
+ellipsis
.user-label
font-size: 1.4rem
.user-ip
position: relative
display: inline-block
font-size: 1rem
line-height: 1.2em
color: #808080
> strong
display: block
.user-connection-status
position: absolute
left: -1rem
top: 50%
margin-top: -.3rem
width: .6rem
height: .6rem
border-radius: 50%
&.disconnected
display: none
&.connecting
background: rgba($blue,.5)
animation: blink .75s infinite
&.connected
background: rgba($green,.8)
select
appearance: none
border: none
font-size: 1rem
color: #808080
padding-right: 10px
outline: none
background: transparent url("../images/select-arrow.svg") no-repeat 66px 50%
// firefox fix - remove arrow from select
text-indent: 0.01px
text-overflow: ''
&:nth-of-type(2)
margin-left: -186px
bottom: 225px
&:nth-of-type(3)
margin-left: 120px
bottom: 225px
&:nth-of-type(4)
margin-left: -186px
bottom: 365px
&:nth-of-type(5)
margin-left: 120px
bottom: 365px
&:nth-of-type(6)
margin-left: -326px
bottom: 180px
&:nth-of-type(7)
margin-left: 260px
bottom: 180px
&:nth-of-type(8)
margin-left: -366px
bottom: 320px
&:nth-of-type(9)
margin-left: 300px
bottom: 320px
&:nth-of-type(10)
margin-left: -436px
bottom: 90px
&:nth-of-type(11)
margin-left: 370px
bottom: 90px
&:nth-of-type(12)
bottom: 400px
&:nth-of-type(13)
margin-left: -236px
bottom: 90px
&:nth-of-type(14)
margin-left: 170px
bottom: 90px
&.you
.peer
bottom: 90px
&.others
.peer
.avatar
cursor: pointer
&:hover, &.hover
transform: scale(1.1, 1.1)
.gravatar
border-color: rgba($blue,.8)
&::after
opacity: 0
position: absolute
pointer-events: none
top: 5px
left: 5px
z-index: 100
content: "L"
color: white
font-size: 3rem
font-weight: bold
background: rgba($green,.8)
border: 1px solid white
transform: scaleX(-1) rotate(-45deg)
// +circle
display: inline-block
width: 66px
height: $user-size - 10
line-height: $user-size - 10
text-align: center
vertical-align: middle
border-radius: 50%
transition: opacity .3s
&.transfer-completed
&::after
opacity: 1
@keyframes blink
0%
opacity: 1
50%
opacity: 0
@keyframes shadow
0%
opacity: 0
50%
opacity: 1
box-shadow: rgba(black, .3) 0 0 15px
================================================
FILE: app/templates/about-app.hbs
================================================
{{#modal-dialog onClose=(action "closeModal" target=currentRoute)}}
<h2 class="logo"><span>ShareDrop</span></h2>
<h3>What is it?</h3>
<p>
ShareDrop is a free, <a href="https://github.com/szimek/sharedrop">open-source</a> web app that allows you to easily and securely share files directly between devices without uploading them to any server first.
</p>
<h3>How to use it?</h3>
<h4>Sharing files between devices in a local network<sup>*</sup></h4>
<p>
To send a file to another device in the same local network, open this page (i.e. <a href="https://www.sharedrop.io">www.sharedrop.io</a>) on both devices. Drag and drop a file directly on another person's avatar or click the avatar and select the file you want to send. The file transfer will start once the recipient accepts the file.
</p>
<h4>Sharing files between devices in different networks</h4>
<p>
To send a file to another device in a different network, click <span class="plus-icon">+</span> button in the upper right corner of the page and follow further instructions.
</p>
<h4>VPNs</h4>
<p>
Sharedrop does not work with VPNs, please deactivate any VPN your device might be using.
</p>
<h3>Security</h3>
<p>ShareDrop uses a secure and encrypted peer-to-peer connection to transfer information about the file (its name and size) and file data itself. This means that this data is never transfered through any intermediate server but directly between the sender and recipient devices. To achieve this, ShareDrop uses a technology called WebRTC (Web Real-Time Communication), which is provided natively by browsers. You can read more about WebRTC security <a href="https://bloggeek.me/is-webrtc-safe/">here</a>.</p>
<h3>Feedback</h3>
<p>Got a problem with using ShareDrop, suggestion how to improve it or just want to say hi? Send an email to <a href="mailto:support@sharedrop.io">support@sharedrop.io</a> or report an issue on <a href="https://github.com/szimek/sharedrop/issues">GitHub</a>.</p>
<div class="actions">
<button {{action "closeModal"}} type="button">Got it!</button>
<p class="note"><sup>*</sup>Devices need to have the same <a href="https://www.google.com/search?q=what+is+my+ip" target="_blank" rel="noopener noreferrer">public IP</a> to see each other.</p>
</div>
{{/modal-dialog}}
================================================
FILE: app/templates/about-room.hbs
================================================
{{#modal-dialog onClose=(action "closeModal" target=currentRoute)}}
<h2 class="logo"><span>ShareDrop</span></h2>
<h3>Share files between devices in different networks</h3>
<p>
Copy provided address and send it to the other person...
</p>
<p>
{{room-url value=currentUrl readonly="readonly" style="display: block; margin: auto;"}}
</p>
<p>
Or you can scan it on the other device.
</p>
<p class="qr-code">
{{qr-code text=currentUrl}}
</p>
<p>
Once the other person open this page in a browser, you'll see each other's avatars.
</p>
<p>
Drag and drop a file directly on other person's avatar or click the avatar and select the file you want to send. The file transfer will start once the recipient accepts the file.
</p>
<div class="actions">
<button {{action "closeModal"}} type="button">Got it!</button>
</div>
{{/modal-dialog}}
================================================
FILE: app/templates/about-you.hbs
================================================
ShareDrop lets you share files with others.
Other people will see you as <b>{{you.label}}</b>.
================================================
FILE: app/templates/application.hbs
================================================
<header class="l-header">
<nav class="navbar">
<h1 class="logo">
<span class="logo-title">ShareDrop</span>
<span class="logo-subtitle">P2P file transfer</span>
</h1>
<ul class="nav">
<li>
<a href="javascript:void(0)" class="icon-create-room" {{action "redirect"}} title="Create a room. You'll leave the room you're currently in.">+</a>
</li>
<li>
<a href="javascript:void(0)" class="icon-help" {{action "openModal" "about_app"}} title="About">?</a>
</li>
</ul>
</nav>
</header>
{{outlet}}
{{outlet "modal"}}
<footer class="l-footer">
<p class="about">{{outlet "about_you"}}</p>
<div class="logos">
<div class="left"></div>
<div class="right">
<iframe title="twitter" loading="lazy" class="twitter" src="https://platform.twitter.com/widgets/tweet_button.html?url=https%3A%2F%2Fwww.sharedrop.io&text=ShareDrop%20%E2%80%93%20easily%20and%20securely%20share%20files%20of%20any%20size%20directly%20between%20devices%20using%20your%20browser&count=none" scrolling="no" frameborder="0" allowtransparency="true" ></iframe>
<a href="https://github.com/szimek/sharedrop" class="github" target="_blank" rel="noopener noreferrer"><span>Github</span></a>
</div>
</div>
</footer>
<a href="https://war.ukraine.ua/" class="ribbon" target="_blank"><span class="visually-hidden">We stand with Ukraine!</span></a>
================================================
FILE: app/templates/components/modal-dialog.hbs
================================================
{{! template-lint-disable no-invalid-interactive }}
<div class="modal-overlay" {{action "close"}}></div>
<div class="modal-body">
{{yield}}
</div>
{{! template-lint-enable no-invalid-interactive }}
================================================
FILE: app/templates/components/peer-widget.hbs
================================================
{{! Sender related messages }}
{{#if hasSelectedFile}}
{{#popover-confirm
onConfirm=(action "sendFileTransferInquiry")
onCancel=(action "cancelFileTransfer")
confirmButtonLabel="Send"
cancelButtonLabel="Cancel"
filename=filename
}}
Do you want to send <strong>"{{filename}}"</strong> to <strong>"{{label}}"</strong>?
{{/popover-confirm}}
{{/if}}
{{#if isAwaitingResponse}}
{{#popover-confirm
onCancel=(action "abortFileTransfer")
cancelButtonLabel="Cancel"
filename=filename
}}
Waiting for <strong>"{{label}}"</strong> to accept…
{{/popover-confirm}}
{{/if}}
{{#if hasDeclinedFileTransfer}}
{{#popover-confirm
onConfirm=(action "cancelFileTransfer")
confirmButtonLabel="Ok"
filename=filename
}}
<strong>"{{label}}"</strong> has declined your request.
{{/popover-confirm}}
{{/if}}
{{#if hasError}}
{{#popover-confirm
onConfirm=(action "cancelFileTransfer")
confirmButtonLabel="Ok"
filename=filename
}}
{{partial errorTemplateName}}
{{/popover-confirm}}
{{/if}}
{{! Recipient related popups }}
{{#if hasReceivedFileInfo}}
{{#popover-confirm
onConfirm=(action "acceptFileTransfer")
onCancel=(action "rejectFileTransfer")
confirmButtonLabel="Save"
cancelButtonLabel="Decline"
filename=filename
}}
<strong>"{{label}}"</strong> wants to send you <strong>"{{filename}}"</strong>.
{{/popover-confirm}}
{{/if}}
<div class="avatar">
{{#if isPreparingFileTransfer}}
<CircularProgress @value={{peer.bundlingProgress}} @color="orange" />
{{else if peer.transfer}}
{{#if peer.transfer.receivingProgress}}
<CircularProgress @value={{peer.transfer.receivingProgress}} @color="blue" />
{{else if peer.transfer.sendingProgress}}
<CircularProgress @value={{peer.transfer.sendingProgress}} @color="blue" />
{{/if}}
{{/if}}
{{peer-avatar peer=peer onFileDrop=(action "uploadFile")}}
</div>
<div class="user-info">
<div class="user-ip">
<div class="user-connection-status {{peer.peer.state}}"></div>
<span>{{peer.label}}</span>
{{#if isPreparingFileTransfer}}
<div>
Bundling files...
<br><br>
TIP: You can archive files on your device beforehand to speed up the operation
</div>
{{else if isReceivingFile}}
<div>Receiving file...</div>
{{else if isSendingFile}}
<div>Sending file...</div>
{{/if}}
</div>
</div>
{{file-field multiple=true onChange=(action "uploadFile")}}
================================================
FILE: app/templates/components/popover-confirm.hbs
================================================
<div class="popover">
<div class="popover-body">
<div class="popover-icon">
<i class={{iconClass}}></i>
</div>
<p>{{yield}}</p>
</div>
<div class="popover-buttons">
{{#if cancelButtonLabel}}
<button type="button" {{action "cancel"}}>{{cancelButtonLabel}}</button>
{{/if}}
{{#if confirmButtonLabel}}
<button type="button" {{action "confirm"}}>{{confirmButtonLabel}}</button>
{{/if}}
</div>
</div>
================================================
FILE: app/templates/components/user-widget.hbs
================================================
<div class="avatar">
<img class="gravatar" src={{user.avatarUrl}} alt={{users.label}} title="peer id: {{user.uuid}}">
</div>
<div class="user-info">
<div class="user-label">You</div>
<div class="user-ip">
<span>{{user.label}}</span>
</div>
</div>
================================================
FILE: app/templates/errors/browser-unsupported.hbs
================================================
<div class="error">
<p>We're really sorry, but your browser is not supported.<br>Please use the latest desktop or Android version of<br><b>Chrome</b>, <b>Opera</b> or <b>Firefox</b>.</p>
</div>
================================================
FILE: app/templates/errors/filesystem-unavailable.hbs
================================================
<div class="error">
<p>Uh oh. Looks like there's some issue and we won't be able<br>to save your files.</p>
<p>If you've opened this app in incognito/private window,<br>try again in a normal one.</p>
</div>
================================================
FILE: app/templates/errors/popovers/connection-failed.hbs
================================================
It was not possible to establish direct connection with the other peer.
================================================
FILE: app/templates/errors/popovers/multiple-files.hbs
================================================
The files you have selected exceed the maximum allowed size of 200MB
<br><br>
TIP: You can send single files without size restriction
================================================
FILE: app/templates/index.hbs
================================================
<main class="l-content">
<div class="user others">
{{#each model as |peer|}}
{{peer-widget peer=peer hasCustomRoomName=hasCustomRoomName webrtc=webrtc}}
{{/each}}
</div>
{{#if you.uuid}}
<div class="user you">
{{user-widget user=you}}
</div>
{{/if}}
<svg class="circles" viewBox="-0.5 -0.5 1140 700">
<circle class="circle" cx="570" cy="570" r="30" stroke="rgba(160,160,160, 1)" />
<circle class="circle" cx="570" cy="570" r="100" stroke="rgba(160,160,160,.9)" />
<circle class="circle" cx="570" cy="570" r="200" stroke="rgba(160,160,160,.8)" />
<circle class="circle" cx="570" cy="570" r="300" stroke="rgba(160,160,160,.7)" />
<circle class="circle" cx="570" cy="570" r="400" stroke="rgba(160,160,160,.6)" />
<circle class="circle" cx="570" cy="570" r="500" stroke="rgba(160,160,160,.5)" />
</svg>
</main>
================================================
FILE: config/dotenv.js
================================================
module.exports = function () {
return {
clientAllowedKeys: ['FIREBASE_URL'],
// Fail build when there is missing any of clientAllowedKeys environment variables.
// By default false.
failOnMissingKey: false,
};
};
================================================
FILE: config/environment.js
================================================
module.exports = function (environment) {
const ENV = {
modulePrefix: 'sharedrop',
environment,
rootURL: '/',
locationType: 'auto',
EmberENV: {
FEATURES: {
// Here you can enable experimental features on an ember canary build
// e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true
},
EXTEND_PROTOTYPES: {
// Prevent Ember Data from overriding Date.parse.
Date: false,
},
},
APP: {
// Here you can pass flags/options to your application instance
// when it is created
},
FIREBASE_URL: process.env.FIREBASE_URL,
exportApplicationGlobal: true,
};
if (environment === 'development') {
// ENV.APP.LOG_RESOLVER = true;
// ENV.APP.LOG_ACTIVE_GENERATION = true;
// ENV.APP.LOG_TRANSITIONS = true;
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
// ENV.APP.LOG_VIEW_LOOKUPS = true;
}
if (environment === 'test') {
// Testem prefers this...
ENV.locationType = 'none';
// keep test console output quieter
ENV.APP.LOG_ACTIVE_GENERATION = false;
ENV.APP.LOG_VIEW_LOOKUPS = false;
ENV.APP.rootElement = '#ember-testing';
ENV.APP.autoboot = false;
}
if (environment === 'production') {
ENV.googleAnalyticsId = 'UA-41889586-2';
}
return ENV;
};
================================================
FILE: config/optional-features.json
================================================
{
"application-template-wrapper": false,
"default-async-observers": true,
"jquery-integration": true,
"template-only-glimmer-components": true
}
================================================
FILE: config/targets.js
================================================
const browsers = [
'last 2 Chrome versions',
'last 2 Firefox versions',
'last 2 Safari versions',
'last 2 iOS versions',
'last 2 Edge versions',
];
module.exports = {
browsers,
};
================================================
FILE: ember-cli-build.js
================================================
/* eslint-env node */
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
module.exports = function (defaults) {
const app = new EmberApp(defaults, {
// Don't include SVG files, because of animal icons being loaded dynamically
fingerprint: {
extensions: ['js', 'css', 'png', 'jpg', 'gif', 'map'],
},
// Generate source maps in production as well
sourcemaps: { enabled: true },
sassOptions: { extension: 'sass' },
SRI: { enabled: false },
});
// Use `app.import` to add additional libraries to the generated
// output files.
//
// If you need to use different assets in different
// environments, specify an object as the first parameter. That
// object's keys should be the environment name and the values
// should be the asset to use in that environment.
//
// If the library that you are including contains AMD or ES6
// modules that you would like to import into your application
// please specify an object with the list of modules as keys
// along with the exports of each module as its value.
app.import('vendor/ba-tiny-pubsub.min.js');
app.import('vendor/filer.min.js');
app.import('vendor/idb.filesystem.min.js');
app.import('vendor/peer.js');
return app.toTree();
};
================================================
FILE: firebase_rules.json
================================================
{
"rules": {
"rooms": {
"$roomid": {
// You can see people in the room only if you are in it as well
".read": "auth != null && data.child('users').hasChild(auth.id)",
"users": {
"$userid": {
// You can modify only your own info
".write": "auth != null && $userid == auth.id",
// Ensure that all required attributes are there
".validate": "newData.hasChildren(['uuid', 'public_ip', 'peer']) && newData.child('peer').hasChildren(['id'])"
}
},
"messages": {
// You can send message to anybody in the room
".write": "auth != null",
"$userid": {
// You can read only messages sent to you
".read": "auth != null && $userid == auth.id"
}
}
}
}
}
}
================================================
FILE: lib/google-analytics/index.js
================================================
const { name } = require('./package');
module.exports = {
name,
isDevelopingAddon() {
return true;
},
contentFor(type, config) {
const id = config.googleAnalyticsId;
if (type === 'head' && id) {
return `
<script async src="https://www.googletagmanager.com/gtag/js?id=${id}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${id}');
</script>
`;
}
return '';
},
};
================================================
FILE: lib/google-analytics/package.json
================================================
{
"name": "google-analytics",
"keywords": [
"ember-addon"
]
}
================================================
FILE: newrelic.js
================================================
/**
* New Relic agent configuration.
*
* See lib/config.defaults.js in the agent distribution for a more complete
* description of configuration variables and their potential values.
*/
exports.config = {
/**
* Array of application names.
*/
app_name: ['ShareDrop'],
logging: {
/**
* Level at which to log. 'trace' is most useful to New Relic when diagnosing
* issues with the agent, 'info' and higher will impose the least overhead on
* production applications.
*/
level: 'info',
},
};
================================================
FILE: package.json
================================================
{
"name": "sharedrop",
"version": "1.0.0",
"private": true,
"description": "P2P file sharing",
"license": "MIT",
"author": "Szymon Nowak",
"directories": {
"doc": "doc",
"test": "tests"
},
"repository": {
"type": "git",
"url": "https://github.com/szimek/sharedrop.git"
},
"scripts": {
"build": "ember build --environment=production",
"lint": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*",
"develop": "yarn install --frozen-lockfile && nf --procfile=Procfile.dev start",
"dev": "yarn develop",
"lint:hbs": "ember-template-lint .",
"lint:js": "eslint .",
"start": "ember serve",
"test": "npm-run-all lint:* test:*",
"test:ember": "ember test"
},
"devDependencies": {
"@ember/jquery": "^1.1.0",
"@ember/optional-features": "^2.0.0",
"@glimmer/component": "^1.0.2",
"@glimmer/tracking": "^1.0.2",
"babel-eslint": "^10.1.0",
"broccoli-asset-rev": "^3.0.0",
"ember-auto-import": "^1.6.0",
"ember-cli": "~3.21.2",
"ember-cli-app-version": "^3.2.0",
"ember-cli-babel": "^7.22.1",
"ember-cli-dependency-checker": "^3.2.0",
"ember-cli-dotenv": "^3.1.0",
"ember-cli-htmlbars": "^5.3.1",
"ember-cli-inject-live-reload": "^2.0.2",
"ember-cli-sass": "^10.0.0",
"ember-cli-sri": "^2.1.1",
"ember-cli-terser": "^4.0.0",
"ember-export-application-global": "^2.0.1",
"ember-fetch": "^8.0.2",
"ember-load-initializers": "^2.1.1",
"ember-maybe-import-regenerator": "^0.1.6",
"ember-qrcode-shim": "^0.4.0",
"ember-qunit": "^4.6.0",
"ember-resolver": "^8.0.2",
"ember-source": "~3.21.3",
"ember-template-lint": "^2.13.0",
"eslint": "^7.10.0",
"eslint-config-airbnb-base": "^14.1.0",
"eslint-config-prettier": "^6.12.0",
"eslint-plugin-ember": "^9.2.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.0.1",
"foreman": "3.0.1",
"husky": "^4.3.0",
"lint-staged": "^10.4.0",
"loader.js": "^4.7.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.2",
"qunit-dom": "^1.5.0",
"sass": "^1.26.11"
},
"dependencies": {
"@sentry/browser": "5.24.2",
"@sentry/integrations": "5.24.2",
"body-parser": "^1.10.0",
"compression": "^1.2.2",
"cookie-parser": "^1.3.3",
"cookie-session": "^1.1.0",
"express": "^4.10.6",
"firebase-token-generator": "~2.0.0",
"jszip": "^3.5.0",
"lodash": "^4.17.20",
"morgan": "^1.5.0",
"newrelic": "^6.13.1",
"stream": "^0.0.2",
"uuid": "^8.3.1"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.js": [
"yarn run prettier --write",
"yarn run lint:hbs",
"yarn run lint:js"
]
},
"engines": {
"node": "^14.0.0"
},
"ember": {
"edition": "octane"
},
"ember-addon": {
"paths": [
"lib/google-analytics"
]
}
}
================================================
FILE: prettier.config.js
================================================
// https://prettier.io/docs/en/options.html
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
trailingComma: 'all',
bracketSpacing: true,
jsxBracketSameLine: false,
arrowParens: 'always',
};
================================================
FILE: public/.gitkeep
================================================
================================================
FILE: public/.well-known/brave-rewards-verification.txt
================================================
This is a Brave Rewards publisher verification file.
Domain: sharedrop.io
Token: d463c371edd92de3a09b0ccf1ce8143b9ee7f8c5611734f7ab58e2b67934b4bb
================================================
FILE: public/crossdomain.xml
================================================
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<!-- Read this: www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html -->
<!-- Most restrictive policy: -->
<site-control permitted-cross-domain-policies="none"/>
<!-- Least restrictive policy: -->
<!--
<site-control permitted-cross-domain-policies="all"/>
<allow-access-from domain="*" to-ports="*" secure="false"/>
<allow-http-request-headers-from domain="*" headers="*" secure="false"/>
-->
</cross-domain-policy>
================================================
FILE: public/robots.txt
================================================
# http://www.robotstxt.org
User-agent: *
Disallow:
================================================
FILE: server.js
================================================
/* eslint-env node */
if (process.env.NODE_ENV === 'production') {
// eslint-disable-next-line global-require
require('newrelic');
}
// Room server
const http = require('http');
const path = require('path');
const express = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const cookieSession = require('cookie-session');
const compression = require('compression');
const { v4: uuidv4 } = require('uuid');
const crypto = require('crypto');
const FirebaseTokenGenerator = require('firebase-token-generator');
const firebaseTokenGenerator = new FirebaseTokenGenerator(
process.env.FIREBASE_SECRET,
);
const app = express();
const secret = process.env.SECRET;
const base = ['dist'];
app.enable('trust proxy');
app.use(logger('combined'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(
cookieSession({
cookie: {
// secure: true,
httpOnly: true,
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
},
secret,
proxy: true,
}),
);
app.use(compression());
//
// Web server
//
base.forEach((dir) => {
const subdirs = ['assets', '.well-known'];
subdirs.forEach((subdir) => {
app.use(
`/${subdir}`,
express.static(`${dir}/${subdir}`, {
maxAge: 31104000000, // ~1 year
}),
);
});
});
//
// API server
//
app.get('/', (req, res) => {
const root = path.join(__dirname, base[0]);
console.log({ root });
res.sendFile(`${root}/index.html`);
});
app.get('/rooms/:id', (req, res) => {
const root = path.join(__dirname, base[0]);
res.sendFile(`${root}/index.html`);
});
app.get('/room', (req, res) => {
const ip = req.headers['cf-connecting-ip'] || req.ip;
const name = crypto.createHmac('md5', secret).update(ip).digest('hex');
res.json({ name });
});
app.get('/auth', (req, res) => {
const ip = req.headers['cf-connecting-ip'] || req.ip;
const uid = uuidv4();
const token = firebaseTokenGenerator.createToken(
{ uid, id: uid }, // will be available in Firebase security rules as 'auth'
{ expires: 32503680000 }, // 01.01.3000 00:00
);
res.json({ id: uid, token, public_ip: ip });
});
http
.createServer(app)
.listen(process.env.PORT)
.on('listening', () => {
console.log(
`Started ShareDrop web server at http://localhost:${process.env.PORT}...`,
);
});
================================================
FILE: sharedrop.crx
================================================
{
"name": "ShareDrop",
"description": "Simple file sharing",
"version": "1",
"app": {
"urls": [
"https:/www.sharedrop.io/"
],
"launch": {
"web_url": "https:/www.sharedrop.io/"
}
},
"icons": {
"128": "sharedrop-icon-128.png"
},
"permissions": [
"notifications"
]
}
================================================
FILE: testem.js
================================================
module.exports = {
test_page: 'tests/index.html?hidepassed',
disable_watching: true,
launch_in_ci: ['Chrome'],
launch_in_dev: ['Chrome'],
browser_start_timeout: 120,
browser_args: {
Chrome: {
ci: [
// --no-sandbox is needed when running Chrome inside a container
process.env.CI ? '--no-sandbox' : null,
'--headless',
'--disable-dev-shm-usage',
'--disable-software-rasterizer',
'--mute-audio',
'--remote-debugging-port=0',
'--window-size=1440,900',
].filter(Boolean),
},
},
};
================================================
FILE: tests/helpers/.gitkeep
================================================
================================================
FILE: tests/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>EmberCliTest Tests</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{content-for "head"}}
{{content-for "test-head"}}
<link rel="stylesheet" href="{{rootURL}}assets/vendor.css">
<link rel="stylesheet" href="{{rootURL}}assets/sharedrop.css">
<link rel="stylesheet" href="{{rootURL}}assets/test-support.css">
{{content-for "head-footer"}}
{{content-for "test-head-footer"}}
</head>
<body>
{{content-for "body"}}
{{content-for "test-body"}}
<script src="/testem.js" integrity=""></script>
<script src="{{rootURL}}assets/vendor.js"></script>
<script src="{{rootURL}}assets/test-support.js"></script>
<script src="{{rootURL}}assets/sharedrop.js"></script>
<script src="{{rootURL}}assets/tests.js"></script>
{{content-for "body-footer"}}
{{content-for "test-body-footer"}}
</body>
</html>
================================================
FILE: tests/integration/.gitkeep
================================================
================================================
FILE: tests/test-helper.js
================================================
/* eslint */
import { setApplication } from '@ember/test-helpers';
import { start } from 'ember-qunit';
import Application from 'sharedrop/app';
import config from 'sharedrop/config/environment';
setApplication(Application.create(config.APP));
start();
================================================
FILE: tests/unit/.gitkeep
================================================
================================================
FILE: vendor/.gitkeep
================================================
================================================
FILE: vendor/peer.js
================================================
/*! peerjs.js build:0.3.7, development. Copyright(c) 2013 Michelle Bu <michelle@michellebu.com> */
(function(exports){
/**
* Light EventEmitter. Ported from Node.js/events.js
* Eric Zhang
*/
/**
* EventEmitter class
* Creates an object with event registering and firing methods
*/
function EventEmitter() {
// Initialise required storage variables
this._events = {};
}
var isArray = Array.isArray;
EventEmitter.prototype.addListener = function(type, listener) {
if ('function' !== typeof listener) {
throw new Error('addListener only takes instances of Function');
}
// To avoid recursion in the case that type == "newListeners"! Before
// adding it to the listeners, first emit "newListeners".
this.emit('newListener', type, typeof listener.listener === 'function' ?
listener.listener : listener);
if (!this._events[type]) {
// Optimize the case of one listener. Don't need the extra array object.
this._events[type] = listener;
} else if (isArray(this._events[type])) {
// If we've already got an array, just append.
this._events[type].push(listener);
} else {
// Adding the second element, need to change to array.
this._events[type] = [this._events[type], listener];
}
return this;
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.once = function(type, listener) {
if ('function' !== typeof listener) {
throw new Error('.once only takes instances of Function');
}
var self = this;
function g() {
self.removeListener(type, g);
listener.apply(this, arguments);
}
g.listener = listener;
self.on(type, g);
return this;
};
EventEmitter.prototype.removeListener = function(type, listener) {
if ('function' !== typeof listener) {
throw new Error('removeListener only takes instances of Function');
}
// does not use listeners(), so no side effect of creating _events[type]
if (!this._events[type]) return this;
var list = this._events[type];
if (isArray(list)) {
var position = -1;
for (var i = 0, length = list.length; i < length; i++) {
if (list[i] === listener ||
(list[i].listener && list[i].listener === listener))
{
position = i;
break;
}
}
if (position < 0) return this;
list.splice(position, 1);
if (list.length === 0)
delete this._events[type];
} else if (list === listener ||
(list.listener && list.listener === listener))
{
delete this._events[type];
}
return this;
};
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.removeAllListeners = function(type) {
if (arguments.length === 0) {
this._events = {};
return this;
}
// does not use listeners(), so no side effect of creating _events[type]
if (type && this._events && this._events[type]) this._events[type] = null;
return this;
};
EventEmitter.prototype.listeners = function(type) {
if (!this._events[type]) this._events[type] = [];
if (!isArray(this._events[type])) {
this._events[type] = [this._events[type]];
}
return this._events[type];
};
EventEmitter.prototype.emit = function(type) {
type = arguments[0];
var handler = this._events[type];
var l, args, i;
if (!handler) return false;
if (typeof handler == 'function') {
switch (arguments.length) {
// fast cases
case 1:
handler.call(this);
break;
case 2:
handler.call(this, arguments[1]);
break;
case 3:
handler.call(this, arguments[1], arguments[2]);
break;
// slower
default:
l = arguments.length;
args = new Array(l - 1);
for (i = 1; i < l; i++) args[i - 1] = arguments[i];
handler.apply(this, args);
}
return true;
} else if (isArray(handler)) {
l = arguments.length;
args = new Array(l - 1);
for (i = 1; i < l; i++) args[i - 1] = arguments[i];
var listeners = handler.slice();
for (i = 0, l = listeners.length; i < l; i++) {
listeners[i].apply(this, args);
}
return true;
} else {
return false;
}
};
exports.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
exports.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
exports.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
var defaultConfig = {
'iceServers': [{
'urls': 'stun:stun.l.google.com:19302',
}]
};
var util = {
noop: function() {},
// Logging logic
logLevel: 0,
setLogLevel: function(level) {
var debugLevel = parseInt(level, 10);
if (!isNaN(parseInt(level, 10))) {
util.logLevel = debugLevel;
} else {
// If they are using truthy/falsy values for debug
util.logLevel = level ? 3 : 0;
}
util.log = util.warn = util.error = util.noop;
if (util.logLevel > 0) {
util.error = util._printWith('ERROR');
}
if (util.logLevel > 1) {
util.warn = util._printWith('WARNING');
}
if (util.logLevel > 2) {
util.log = util._print;
}
},
setLogFunction: function(fn) {
if (fn.constructor !== Function) {
util.warn('The log function you passed in is not a function. Defaulting to regular logs.');
} else {
util._print = fn;
}
},
_printWith: function(prefix) {
return function() {
var copy = Array.prototype.slice.call(arguments);
copy.unshift(prefix);
util._print.apply(util, copy);
};
},
_print: function () {
var err = false;
var copy = Array.prototype.slice.call(arguments);
copy.unshift('PeerJS: ');
for (var i = 0, l = copy.length; i < l; i++){
if (copy[i] instanceof Error) {
copy[i] = '(' + copy[i].name + ') ' + copy[i].message;
err = true;
}
}
err ? console.error.apply(console, copy) : console.log.apply(console, copy);
},
//
// Returns browser-agnostic default config
defaultConfig: defaultConfig,
//
// Returns the current browser.
browser: (function() {
if (window.mozRTCPeerConnection) {
return 'Firefox';
} else if (window.webkitRTCPeerConnection) {
return 'Chrome';
} else if (window.RTCPeerConnection) {
return 'Supported';
} else {
return 'Unsupported';
}
})(),
//
// Lists which features are supported
supports: (function() {
if (typeof RTCPeerConnection === 'undefined') {
return {};
}
var data = true;
var audioVideo = true;
var binaryBlob = false;
var sctp = true;
var onnegotiationneeded = !!window.webkitRTCPeerConnection;
var pc, dc;
try {
pc = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]});
} catch (e) {
data = false;
audioVideo = false;
}
if (data) {
try {
dc = pc.createDataChannel('_PEERJSTEST');
} catch (e) {
sctp = false;
data = false;
}
}
if (data) {
// Binary test
try {
dc.binaryType = 'blob';
binaryBlob = true;
} catch (e) {
console.log(e);
}
}
// FIXME: not really the best check...
if (audioVideo) {
audioVideo = !!pc.addStream;
}
// FIXME: this is not great because in theory it doesn't work for
// av-only browsers (?).
if (!onnegotiationneeded && data) {
// sync default check.
var negotiationPC = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]});
negotiationPC.onnegotiationneeded = function() {
onnegotiationneeded = true;
// async check.
if (util && util.supports) {
util.supports.onnegotiationneeded = true;
}
};
negotiationPC.createDataChannel('_PEERJSNEGOTIATIONTEST');
setTimeout(function() {
negotiationPC.close();
}, 1000);
}
if (pc) {
pc.close();
}
return {
audioVideo: audioVideo,
data: data,
binaryBlob: binaryBlob,
binary: sctp, // deprecated; sctp implies binary support.
reliable: sctp, // deprecated; sctp implies reliable data.
sctp: sctp,
onnegotiationneeded: onnegotiationneeded
};
}()),
//
// Ensure alphanumeric ids
validateId: function(id) {
// Allow empty ids
return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(id);
},
debug: false,
inherits: function(ctor, superCtor) {
ctor.super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
},
extend: function(dest, source) {
for(var key in source) {
if(source.hasOwnProperty(key)) {
dest[key] = source[key];
}
}
return dest;
},
log: function () {
if (util.debug) {
var err = false;
var copy = Array.prototype.slice.call(arguments);
copy.unshift('PeerJS: ');
for (var i = 0, l = copy.length; i < l; i++){
if (copy[i] instanceof Error) {
copy[i] = '(' + copy[i].name + ') ' + copy[i].message;
err = true;
}
}
err ? console.error.apply(console, copy) : console.log.apply(console, copy);
}
},
setZeroTimeout: (function(global) {
var timeouts = [];
var messageName = 'zero-timeout-message';
// Like setTimeout, but only takes a function argument. There's
// no time argument (always zero) and no arguments (you have to
// use a closure).
function setZeroTimeoutPostMessage(fn) {
timeouts.push(fn);
global.postMessage(messageName, '*');
}
function handleMessage(event) {
if (event.source == global && event.data == messageName) {
if (event.stopPropagation) {
event.stopPropagation();
}
if (timeouts.length) {
timeouts.shift()();
}
}
}
if (global.addEventListener) {
global.addEventListener('message', handleMessage, true);
} else if (global.attachEvent) {
global.attachEvent('onmessage', handleMessage);
}
return setZeroTimeoutPostMessage;
}(this)),
randomToken: function () {
return Math.random().toString(36).substr(2);
},
isSecure: function() {
return location.protocol === 'https:';
}
};
exports.util = util;
/**
* A peer who can initiate connections with other peers.
*/
function Peer(id, options) {
EventEmitter.call(this);
// Configurize options
this.options = util.extend({
debug: 0, // 1: Errors, 2: Warnings, 3: All logs
config: util.defaultConfig
}, options);
// Set a custom log function if present
if (options.logFunction) {
util.setLogFunction(options.logFunction);
}
util.setLogLevel(options.debug);
// Sanity checks
// Ensure WebRTC supported
if (!util.supports.audioVideo && !util.supports.data ) {
this._delayedAbort('browser-incompatible', 'The current browser does not support WebRTC');
return;
}
// Ensure alphanumeric id
if (!util.validateId(id)) {
this._delayedAbort('invalid-id', 'ID "' + id + '" is invalid');
return;
}
// States
this.destroyed = false; // Connections have been killed
this.disconnected = false; // Connection to PeerServer killed manually but P2P connections still active
this.open = false; // Sockets and such are not yet open.
// References
this.connections = {}; // DataConnections for this peer.
this._lostMessages = {}; // src => [list of messages]
// Start the connections
this._initialize(id);
}
util.inherits(Peer, EventEmitter);
/** Initialize a connection with the server. */
Peer.prototype._initialize = function (id) {
this.id = id;
// Firebase
this._ref = this.options.firebaseRef;
this._connectionRef = this._ref.child('.info/connected');
this._messagesRef = this._ref.child('rooms/' + this.options.room + '/messages');
this._receivedMessagesRef = this._messagesRef.child(id);
this._connectionRef.on('value', (connectionSnapshot) => {
if (connectionSnapshot.val() === true) {
// Remove received messages on disconnect
this._receivedMessagesRef.onDisconnect().remove();
// Listen to incoming messages
this._receivedMessagesRef.on('child_added', (snapshot) => {
const message = snapshot.val();
this._handleMessage(message);
});
} else {
this._receivedMessagesRef.off();
}
});
// The connection to the server is open
this.emit('open', this.id);
this.open = true;
};
/** Handles messages from the server. */
Peer.prototype._handleMessage = function (message) {
var type = message.type;
var payload = message.payload;
var peer = message.src;
var connectionId, connection;
switch (type) {
case 'LEAVE': // Another peer has closed its connection to this peer.
util.log('Received leave message from', peer);
this._cleanupPeer(peer);
break;
case 'EXPIRE': // The offer sent to a peer has expired without response.
this.emit('error', new Error('Could not connect to peer ' + peer));
break;
case 'OFFER': // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
connectionId = payload.connectionId;
connection = this.getConnection(peer, connectionId);
if (connection) {
util.warn('Offer received for existing Connection ID:', connectionId);
//connection.handleMessage(message);
} else {
// Create a new connection.
if (payload.type === 'media') {
connection = new MediaConnection(peer, this, {
connectionId: connectionId,
_payload: payload,
metadata: payload.metadata
});
this._addConnection(peer, connection);
this.emit('call', connection);
} else if (payload.type === 'data') {
connection = new DataConnection(peer, this, {
connectionId: connectionId,
_payload: payload,
metadata: payload.metadata,
label: payload.label,
serialization: payload.serialization,
reliable: payload.reliable
});
this._addConnection(peer, connection);
this.emit('connection', connection);
} else {
util.warn('Received malformed connection type:', payload.type);
return;
}
// Find messages.
var messages = this._getMessages(connectionId);
for (var i = 0, ii = messages.length; i < ii; i += 1) {
connection.handleMessage(messages[i]);
}
}
break;
case 'ERROR':
connectionId = payload.connectionId;
connection = this.getConnection(peer, connectionId);
var error = new Error(payload.message);
error.type = payload.type;
if (connection) {
connection.emit('error', error);
}
break;
default:
if (!payload) {
util.warn('You received a malformed message from ' + peer + ' of type ' + type);
return;
}
var id = payload.connectionId;
connection = this.getConnection(peer, id);
if (connection && connection.pc) {
// Pass it on.
connection.handleMessage(message);
} else if (id) {
// Store for possible later use
this._storeMessage(id, message);
} else {
util.warn('You received an unrecognized message:', message);
}
break;
}
};
/** Stores messages without a set up connection, to be claimed later. */
Peer.prototype._storeMessage = function (connectionId, message) {
if (!this._lostMessages[connectionId]) {
this._lostMessages[connectionId] = [];
}
this._lostMessages[connectionId].push(message);
};
/** Retrieve messages from lost message store */
Peer.prototype._getMessages = function (connectionId) {
var messages = this._lostMessages[connectionId];
if (messages) {
delete this._lostMessages[connectionId];
return messages;
} else {
return [];
}
};
/**
* Returns a DataConnection to the specified peer. See documentation for a
* complete list of options.
*/
Peer.prototype.connect = function(peer, options) {
if (this.disconnected) {
util.warn('You cannot connect to a new Peer because you called '
+ '.disconnect() on this Peer and ended your connection with the'
+ ' server. You can create a new Peer to reconnect.');
this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.'));
return;
}
var connection = new DataConnection(peer, this, options);
this._addConnection(peer, connection);
return connection;
};
/**
* Returns a MediaConnection to the specified peer. See documentation for a
* complete list of options.
*/
Peer.prototype.call = function(peer, stream, options) {
if (this.disconnected) {
util.warn('You cannot connect to a new Peer because you called '
+ '.disconnect() on this Peer and ended your connection with the'
+ ' server. You can create a new Peer to reconnect.');
this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.'));
return;
}
if (!stream) {
util.error('To call a peer, you must provide a stream from your browser\'s `getUserMedia`.');
return;
}
options = options || {};
options._stream = stream;
var call = new MediaConnection(peer, this, options);
this._addConnection(peer, call);
return call;
};
/** Add a data/media connection to this peer. */
Peer.prototype._addConnection = function(peer, connection) {
if (!this.connections[peer]) {
this.connections[peer] = [];
}
this.connections[peer].push(connection);
};
/** Retrieve a data/media connection for this peer. */
Peer.prototype.getConnection = function(peer, id) {
var connections = this.connections[peer];
if (!connections) {
return null;
}
for (var i = 0, ii = connections.length; i < ii; i++) {
if (connections[i].id === id) {
return connections[i];
}
}
return null;
};
Peer.prototype._delayedAbort = function(type, message) {
var self = this;
util.setZeroTimeout(function (){
self._abort(type, message);
});
};
/** Destroys the Peer and emits an error message. */
Peer.prototype._abort = function(type, message) {
util.error('Aborting. Error:', message);
var err = new Error(message);
err.type = type;
this.destroy();
this.emit('error', err);
};
/**
* Destroys the Peer: closes all active connections as well as the connection
* to the server.
* Warning: The peer can no longer create or accept connections after being
* destroyed.
*/
Peer.prototype.destroy = function() {
if (!this.destroyed) {
this._cleanup();
this.destroyed = true;
}
};
/** Disconnects every connection on this peer. */
Peer.prototype._cleanup = function() {
if (this.connections) {
var peers = Object.keys(this.connections);
for (var i = 0, ii = peers.length; i < ii; i++) {
this._cleanupPeer(peers[i]);
}
}
this.emit('close');
};
/** Closes all connections to this peer. */
Peer.prototype._cleanupPeer = function(peer) {
var connections = this.connections[peer];
for (var j = 0, jj = connections.length; j < jj; j += 1) {
connections[j].close();
}
};
exports.Peer = Peer;
/**
* Wraps a DataChannel between two Peers.
*/
function DataConnection(peer, provider, options) {
if (!(this instanceof DataConnection)) return new DataConnection(peer, provider, options);
EventEmitter.call(this);
this.options = util.extend({
serialization: 'binary',
reliable: false,
metadata: {} // Firebase doesn't allow undefined values
}, options);
// Connection is not open yet.
this.open = false;
this.type = 'data';
this.peer = peer;
this.provider = provider;
this.id = this.options.connectionId || DataConnection._idPrefix + util.randomToken();
this.label = this.options.label || this.id;
this.metadata = this.options.metadata;
this.serialization = this.options.serialization;
this.reliable = this.options.reliable;
// Data channel buffering.
this._buffer = [];
this._buffering = false;
this.bufferSize = 0;
// For storing large data.
this._chunkedData = {};
if (this.options._payload) {
this._peerBrowser = this.options._payload.browser;
}
Negotiator.startConnection(
this,
this.options._payload || {
originator: true
}
);
}
util.inherits(DataConnection, EventEmitter);
DataConnection._idPrefix = 'dc_';
/** Called by the Negotiator when the DataChannel is ready. */
DataConnection.prototype.initialize = function(dc) {
this._dc = this.dataChannel = dc;
this._configureDataChannel();
};
DataConnection.prototype._configureDataChannel = function() {
var self = this;
if (util.supports.sctp) {
this._dc.binaryType = 'arraybuffer';
}
this._dc.onopen = function() {
util.log('Data channel connection success');
self.open = true;
self.emit('open');
};
this._dc.onmessage = function (e) {
self._handleDataMessage(e);
};
this._dc.onclose = function (e) {
util.log('DataChannel closed for:', self.peer);
util.log(e);
self.close();
};
};
// Handles a DataChannel message.
DataConnection.prototype._handleDataMessage = function(e) {
var data = e.data;
if (data.byteLength !== undefined) {
// ArrayBuffer
} else {
// String (JSON)
data = JSON.parse(data);
}
this.emit('data', data);
};
/**
* Exposed functionality for users.
*/
/** Allows user to close connection. */
DataConnection.prototype.close = function() {
if (!this.open) {
return;
}
this.open = false;
Negotiator.cleanup(this);
this.emit('close');
};
/** Allows user to send data. */
DataConnection.prototype.send = function(data) {
if (!this.open) {
this.emit('error', new Error('Connection is not open. You should listen for the `open` event before sending messages.'));
return;
}
// Lame type check
if (data.byteLength === undefined) {
// JSON string
data = JSON.stringify(data);
}
this._bufferedSend(data);
};
DataConnection.prototype._bufferedSend = function(msg) {
if (this._buffering || !this._trySend(msg)) {
this._buffer.push(msg);
this.bufferSize = this._buffer.length;
}
};
// Returns true if the send succeeds.
DataConnection.prototype._trySend = function(msg) {
try {
this._dc.send(msg);
} catch (e) {
this._buffering = true;
var self = this;
setTimeout(function() {
// Try again.
self._buffering = false;
self._tryBuffer();
}, 100);
return false;
}
return true;
};
// Try to send the first message in the buffer.
DataConnection.prototype._tryBuffer = function() {
if (this._buffer.length === 0) {
return;
}
var msg = this._buffer[0];
if (this._trySend(msg)) {
this._buffer.shift();
this.bufferSize = this._buffer.length;
this._tryBuffer();
}
};
DataConnection.prototype.handleMessage = function(message) {
var payload = message.payload;
switch (message.type) {
case 'ANSWER':
this._peerBrowser = payload.browser;
// Forward to negotiator
Negotiator.handleSDP(message.type, this, payload.sdp);
break;
case 'CANDIDATE':
Negotiator.handleCandidate(this, payload.candidate);
break;
default:
util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer);
break;
}
};
/**
* Wraps the streaming interface between two Peers.
*/
function MediaConnection(peer, provider, options) {
if (!(this instanceof MediaConnection)) return new MediaConnection(peer, provider, options);
EventEmitter.call(this);
this.options = util.extend({}, options);
this.open = false;
this.type = 'media';
this.peer = peer;
this.provider = provider;
this.metadata = this.options.metadata;
this.localStream = this.options._stream;
this.id = this.options.connectionId || MediaConnection._idPrefix + util.randomToken();
if (this.localStream) {
Negotiator.startConnection(
this,
{_stream: this.localStream, originator: true}
);
}
}
util.inherits(MediaConnection, EventEmitter);
MediaConnection._idPrefix = 'mc_';
MediaConnection.prototype.addStream = function(remoteStream) {
util.log('Receiving stream', remoteStream);
this.remoteStream = remoteStream;
this.emit('stream', remoteStream); // Should we call this `open`?
};
MediaConnection.prototype.handleMessage = function(message) {
var payload = message.payload;
switch (message.type) {
case 'ANSWER':
// Forward to negotiator
Negotiator.handleSDP(message.type, this, payload.sdp);
this.open = true;
break;
case 'CANDIDATE':
Negotiator.handleCandidate(this, payload.candidate);
break;
default:
util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer);
break;
}
};
MediaConnection.prototype.answer = function(stream) {
if (this.localStream) {
util.warn('Local stream already exists on this MediaConnection. Are you answering a call twice?');
return;
}
this.options._payload._stream = stream;
this.localStream = stream;
Negotiator.startConnection(
this,
this.options._payload
);
// Retrieve lost messages stored because PeerConnection not set up.
var messages = this.provider._getMessages(this.id);
for (var i = 0, ii = messages.length; i < ii; i += 1) {
this.handleMessage(messages[i]);
}
this.open = true;
};
/**
* Exposed functionality for users.
*/
/** Allows user to close connection. */
MediaConnection.prototype.close = function() {
if (!this.open) {
return;
}
this.open = false;
Negotiator.cleanup(this);
this.emit('close');
};
/**
* Manages all negotiations between Peers.
*/
var Negotiator = {
pcs: {
data: {},
media: {}
}, // type => {peerId: {pc_id: pc}}.
//providers: {}, // provider's id => providers (there may be multiple providers/client.
queue: [] // connections that are delayed due to a PC being in use.
};
Negotiator._idPrefix = 'pc_';
/** Returns a PeerConnection object set up correctly (for data, media). */
Negotiator.startConnection = function(connection, options) {
var pc = Negotiator._getPeerConnection(connection, options);
if (connection.type === 'media' && options._stream) {
// Add the stream.
pc.addStream(options._stream);
}
// Set the connection's PC.
connection.pc = pc;
// What do we need to do now?
if (options.originator) {
if (connection.type === 'data') {
// Create the datachannel.
var config = {};
// Dropping reliable:false support, since it seems to be crashing
// Chrome.
/*if (util.supports.sctp && !options.reliable) {
// If we have canonical reliable support...
config = {maxRetransmits: 0};
}*/
// Fallback to ensure older browsers don't crash.
if (!util.supports.sctp) {
config = {reliable: options.reliable};
}
var dc = pc.createDataChannel(connection.label, config);
connection.initialize(dc);
}
if (!util.supports.onnegotiationneeded) {
Negotiator._makeOffer(connection);
}
} else {
Negotiator.handleSDP('OFFER', connection, options.sdp);
}
};
Negotiator._getPeerConnection = function (connection, options) {
if (!Negotiator.pcs[connection.type]) {
util.error(connection.type + ' is not a valid connection type. Maybe you overrode the `type` property somewhere.');
}
if (!Negotiator.pcs[connection.type][connection.peer]) {
Negotiator.pcs[connection.type][connection.peer] = {};
}
var pc;
if (options.pc) { // Simplest case: PC id already provided for us.
pc = Negotiator.pcs[connection.type][connection.peer][options.pc];
}
if (!pc || pc.signalingState !== 'stable') {
pc = Negotiator._startPeerConnection(connection);
}
return pc;
};
/** Start a PC. */
Negotiator._startPeerConnection = function(connection) {
util.log('Creating RTCPeerConnection.');
var id = Negotiator._idPrefix + util.randomToken();
var optional = {};
if (connection.type === 'data' && !util.supports.sctp) {
optional = {optional: [{RtpDataChannels: true}]};
} else if (connection.type === 'media') {
// Interop req for chrome.
optional = {optional: [{DtlsSrtpKeyAgreement: true}]};
}
var pc = new RTCPeerConnection(connection.provider.options.config, optional);
Negotiator.pcs[connection.type][connection.peer][id] = pc;
Negotiator._setupListeners(connection, pc, id);
return pc;
};
/** Set up various WebRTC listeners. */
Negotiator._setupListeners = function(connection, pc) {
var provider = connection.provider,
src = provider.id,
dst = connection.peer,
connectionId = connection.id;
// ICE CANDIDATES.
util.log('Listening for ICE candidates.');
pc.onicecandidate = function(evt) {
if (evt.candidate) {
util.log('Received ICE candidates for:', dst);
// For some reason Firefox requires toJSON call
var c = evt.candidate,
candidate = 'toJSON' in c ? c.toJSON() : c;
provider._messagesRef.child(dst).push({
type: 'CANDIDATE',
payload: {
candidate: candidate,
type: connection.type,
connectionId: connectionId
},
src: src,
dst: dst
});
}
};
pc.oniceconnectionstatechange = function() {
var error;
switch (pc.iceConnectionState) {
case 'failed':
util.log("ICE connection state is 'failed', notifying " + dst);
error = new Error("ICE connection state is 'failed'");
error.type = 'failed';
connection.provider._messagesRef.child(dst).push({
type: 'ERROR',
payload: {
message: error.message,
type: error.type,
connectionId: connectionId
},
src: src,
dst: dst
});
connection.emit('error', error);
connection.close();
break;
case 'disconnected':
util.log("ICE connection state is 'disconnected', closing connections to " + dst);
error = new Error("ICE connection state is 'disconnected'");
error.type = 'disconnected';
connection.emit('error', error);
connection.close();
break;
case 'completed':
pc.onicecandidate = util.noop;
break;
}
};
// Fallback for older Chrome impls.
pc.onicechange = pc.oniceconnectionstatechange;
// ONNEGOTIATIONNEEDED (Chrome)
util.log('Listening for `negotiationneeded`');
pc.onnegotiationneeded = function() {
util.log('`negotiationneeded` triggered');
if (pc.signalingState == 'stable') {
Negotiator._makeOffer(connection);
} else {
util.log('onnegotiationneeded triggered when not stable. Is another connection being established?');
}
};
// DATACONNECTION.
util.log('Listening for data channel');
// Fired between offer and answer, so options should already be saved
// in the options hash.
pc.ondatachannel = function(evt) {
util.log('Received data channel');
var dc = evt.channel;
var connection = provider.getConnection(dst, connectionId);
connection.initialize(dc);
};
// MEDIACONNECTION.
util.log('Listening for remote stream');
pc.onaddstream = function(evt) {
util.log('Received remote stream');
var stream = evt.stream;
provider.getConnection(dst, connectionId).addStream(stream);
};
};
Negotiator.cleanup = function(connection) {
util.log('Cleaning up PeerConnection to ' + connection.peer);
var pc = connection.pc;
if (!!pc && (pc.readyState !== 'closed' || pc.signalingState !== 'closed')) {
pc.close();
connection.pc = null;
}
};
Negotiator._makeOffer = function(connection) {
var pc = connection.pc,
src = connection.provider.id,
dst = connection.peer;
pc
.createOffer(connection.options.constraints || {})
.then(function (offer) {
util.log('Created offer.');
pc
.setLocalDescription(offer)
.then(function () {
// For some reason Firefox requires toJSON call
var o = offer;
offer = 'toJSON' in o ? o.toJSON() : o;
util.log('Set localDescription: offer', 'for:', dst);
connection.provider._messagesRef.child(dst).push({
type: 'OFFER',
payload: {
sdp: offer,
type: connection.type,
label: connection.label,
connectionId: connection.id,
reliable: connection.reliable,
serialization: connection.serialization,
metadata: connection.metadata,
browser: util.browser
},
src: src,
dst: dst
});
})
.catch(function (err) {
connection.provider.emit('error', err);
util.log('Failed to setLocalDescription, ', err);
});
})
.catch(function (err) {
connection.provider.emit('error', err);
util.log('Failed to createOffer, ', err);
});
};
Negotiator._makeAnswer = function(connection) {
var pc = connection.pc,
provider = connection.provider,
src = provider.id,
dst = connection.peer;
pc
.createAnswer()
.then(function(answer) {
util.log('Created answer.');
pc
.setLocalDescription(answer)
.then(function() {
// For some reason Firefox requires toJSON call
var a = answer;
answer = 'toJSON' in a ? a.toJSON() : a;
util.log('Set localDescription: answer', 'for:', dst);
provider._messagesRef.child(dst).push({
type: 'ANSWER',
payload: {
sdp: answer,
type: connection.type,
connectionId: connection.id,
browser: util.browser
},
src: src,
dst: dst
});
})
.catch(function(err) {
connection.provider.emit('error', err);
util.log('Failed to setLocalDescription, ', err);
});
})
.catch(function(err) {
connection.provider.emit('error', err);
util.log('Failed to create answer, ', err);
});
};
/** Handle an SDP. */
Negotiator.handleSDP = function(type, connection, sdp) {
sdp = new RTCSessionDescription(sdp);
var pc = connection.pc;
util.log('Setting remote description', sdp);
pc
.setRemoteDescription(sdp)
.then(function() {
util.log('Set remoteDescription:', type, 'for:', connection.peer);
if (type === 'OFFER') {
Negotiator._makeAnswer(connection);
}
})
.catch(function(err) {
connection.provider.emit('error', err);
util.log('Failed to setRemoteDescription, ', err);
});
};
/** Handle a candidate. */
Negotiator.handleCandidate = function(connection, ice) {
var candidate = ice.candidate;
var sdpMLineIndex = ice.sdpMLineIndex;
connection.pc.addIceCandidate(new RTCIceCandidate({
sdpMLineIndex: sdpMLineIndex,
candidate: candidate
}));
util.log('Added ICE candidate for:', connection.peer);
};
})(this);
gitextract_vxj0u4do/
├── .editorconfig
├── .ember-cli
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .template-lintrc.js
├── .travis.yml
├── .watchmanconfig
├── Dockerfile
├── LICENSE
├── Procfile
├── Procfile.dev
├── README.md
├── app/
│ ├── app.js
│ ├── components/
│ │ ├── circular-progress.hbs
│ │ ├── circular-progress.js
│ │ ├── file-field.js
│ │ ├── modal-dialog.js
│ │ ├── peer-avatar.js
│ │ ├── peer-widget.js
│ │ ├── popover-confirm.js
│ │ ├── room-url.js
│ │ └── user-widget.js
│ ├── controllers/
│ │ ├── application.js
│ │ └── index.js
│ ├── helpers/
│ │ └── is-equal.js
│ ├── index.html
│ ├── initializers/
│ │ └── prerequisites.js
│ ├── models/
│ │ ├── peer.js
│ │ └── user.js
│ ├── router.js
│ ├── routes/
│ │ ├── application.js
│ │ ├── error.js
│ │ ├── index.js
│ │ └── room.js
│ ├── services/
│ │ ├── analytics.js
│ │ ├── avatar.js
│ │ ├── file.js
│ │ ├── room.js
│ │ └── web-rtc.js
│ ├── styles/
│ │ ├── app.sass
│ │ ├── base/
│ │ │ ├── _element_defaults.sass
│ │ │ ├── _glyphicons_filetypes.sass
│ │ │ ├── _mixins.sass
│ │ │ ├── _reset.sass
│ │ │ └── _variables.sass
│ │ ├── layout/
│ │ │ ├── _content.sass
│ │ │ ├── _footer.sass
│ │ │ ├── _header.sass
│ │ │ └── _media.sass
│ │ └── modules/
│ │ ├── _modal.sass
│ │ ├── _modules.sass
│ │ ├── _popover.sass
│ │ └── _users.sass
│ └── templates/
│ ├── about-app.hbs
│ ├── about-room.hbs
│ ├── about-you.hbs
│ ├── application.hbs
│ ├── components/
│ │ ├── modal-dialog.hbs
│ │ ├── peer-widget.hbs
│ │ ├── popover-confirm.hbs
│ │ └── user-widget.hbs
│ ├── errors/
│ │ ├── browser-unsupported.hbs
│ │ ├── filesystem-unavailable.hbs
│ │ └── popovers/
│ │ ├── connection-failed.hbs
│ │ └── multiple-files.hbs
│ └── index.hbs
├── config/
│ ├── dotenv.js
│ ├── environment.js
│ ├── optional-features.json
│ └── targets.js
├── ember-cli-build.js
├── firebase_rules.json
├── lib/
│ └── google-analytics/
│ ├── index.js
│ └── package.json
├── newrelic.js
├── package.json
├── prettier.config.js
├── public/
│ ├── .gitkeep
│ ├── .well-known/
│ │ └── brave-rewards-verification.txt
│ ├── crossdomain.xml
│ └── robots.txt
├── server.js
├── sharedrop.crx
├── testem.js
├── tests/
│ ├── helpers/
│ │ └── .gitkeep
│ ├── index.html
│ ├── integration/
│ │ └── .gitkeep
│ ├── test-helper.js
│ └── unit/
│ └── .gitkeep
└── vendor/
├── .gitkeep
└── peer.js
SYMBOL INDEX (86 symbols across 23 files)
FILE: app/app.js
class App (line 14) | class App extends Application {
FILE: app/components/circular-progress.js
constant COLORS (line 4) | const COLORS = {
class CircularProgress (line 9) | class CircularProgress extends Component {
method constructor (line 10) | constructor(owner, args) {
method path (line 17) | get path() {
FILE: app/components/file-field.js
method click (line 8) | click(event) {
method change (line 12) | change(event) {
method reset (line 22) | reset() {
FILE: app/components/modal-dialog.js
method close (line 5) | close() {
FILE: app/components/peer-avatar.js
method toggleTransferCompletedClass (line 22) | toggleTransferCompletedClass() {
method init (line 40) | init(...args) {
method didInsertElement (line 48) | didInsertElement(...args) {
method willDestroyElement (line 56) | willDestroyElement(...args) {
method click (line 65) | click() {
method dragEnter (line 72) | dragEnter(event) {
method dragOver (line 78) | dragOver(event) {
method dragLeave (line 82) | dragLeave() {
method drop (line 86) | drop(event) {
method cancelEvent (line 106) | cancelEvent(event) {
method canSendFile (line 111) | canSendFile() {
method isTransferableBundle (line 122) | isTransferableBundle(files) {
FILE: app/components/peer-widget.js
method uploadFile (line 51) | uploadFile(data) {
method sendFileTransferInquiry (line 66) | sendFileTransferInquiry() {
method cancelFileTransfer (line 74) | cancelFileTransfer() {
method abortFileTransfer (line 78) | abortFileTransfer() {
method acceptFileTransfer (line 87) | acceptFileTransfer() {
method rejectFileTransfer (line 98) | rejectFileTransfer() {
method _cancelFileTransfer (line 107) | _cancelFileTransfer() {
method _sendFileTransferResponse (line 116) | _sendFileTransferResponse(response) {
method _reduceFiles (line 124) | async _reduceFiles(files) {
FILE: app/components/popover-confirm.js
method confirm (line 23) | confirm() {
method cancel (line 27) | cancel() {
FILE: app/components/room-url.js
method didInsertElement (line 7) | didInsertElement() {
method copyValueToClipboard (line 11) | copyValueToClipboard() {
FILE: app/controllers/application.js
method init (line 10) | init(...args) {
method redirect (line 28) | redirect() {
FILE: app/controllers/index.js
method _onRoomConnected (line 14) | _onRoomConnected(event, data) {
method _onRoomDisconnected (line 33) | _onRoomDisconnected() {
method _onRoomUserAdded (line 38) | _onRoomUserAdded(event, data) {
method _addPeer (line 46) | _addPeer(attrs) {
method _onRoomUserChanged (line 57) | _onRoomUserChanged(event, data) {
method _onRoomUserRemoved (line 76) | _onRoomUserRemoved(event, data) {
method _onPeerP2PIncomingConnection (line 83) | _onPeerP2PIncomingConnection(event, data) {
method _onPeerDCIncomingConnection (line 93) | _onPeerDCIncomingConnection(event, data) {
method _onPeerDCIncomingConnectionError (line 101) | _onPeerDCIncomingConnectionError(event, data) {
method _onPeerP2POutgoingConnection (line 123) | _onPeerP2POutgoingConnection(event, data) {
method _onPeerDCOutgoingConnection (line 134) | _onPeerDCOutgoingConnection(event, data) {
method _onPeerDCOutgoingConnectionError (line 149) | _onPeerDCOutgoingConnectionError(event, data) {
method _onPeerP2PDisconnected (line 168) | _onPeerP2PDisconnected(event, data) {
method _onPeerP2PFileInfo (line 179) | _onPeerP2PFileInfo(event, data) {
method _onPeerP2PFileResponse (line 190) | _onPeerP2PFileResponse(event, data) {
method _onPeerP2PFileCanceled (line 211) | _onPeerP2PFileCanceled(event, data) {
method _onPeerP2PFileReceived (line 222) | _onPeerP2PFileReceived(event, data) {
method _onPeerP2PFileSent (line 236) | _onPeerP2PFileSent(event, data) {
FILE: app/initializers/prerequisites.js
function initialize (line 9) | function initialize(application) {
FILE: app/models/peer.js
method init (line 12) | init(...args) {
FILE: app/models/user.js
method serialize (line 4) | serialize() {
FILE: app/router.js
class Router (line 4) | class Router extends EmberRouter {
FILE: app/routes/application.js
method setupController (line 4) | setupController(controller) {
method openModal (line 9) | openModal(modalName) {
method closeModal (line 16) | closeModal() {
FILE: app/routes/error.js
method renderTemplate (line 4) | renderTemplate(controller, error) {
FILE: app/routes/index.js
method beforeModel (line 7) | beforeModel() {
method model (line 15) | model() {
method setupController (line 20) | setupController(ctrl, model) {
method renderTemplate (line 69) | renderTemplate() {
method willTransition (line 85) | willTransition() {
FILE: app/routes/room.js
method model (line 6) | model(params) {
method afterModel (line 11) | afterModel(model, transition) {
method setupController (line 19) | setupController(ctrl, model) {
method renderTemplate (line 26) | renderTemplate(ctrl) {
FILE: app/services/analytics.js
method trackEvent (line 2) | trackEvent(name, parameters) {
FILE: app/services/avatar.js
constant AVATARS (line 5) | const AVATARS = [
constant PREFIXES (line 108) | const PREFIXES = [
method get (line 170) | get() {
FILE: app/services/file.js
function rm (line 40) | function rm(entry) {
function finish (line 121) | function finish(link) {
FILE: lib/google-analytics/index.js
method isDevelopingAddon (line 6) | isDevelopingAddon() {
method contentFor (line 10) | contentFor(type, config) {
FILE: vendor/peer.js
function EventEmitter (line 12) | function EventEmitter() {
function g (line 53) | function g() {
function setZeroTimeoutPostMessage (line 376) | function setZeroTimeoutPostMessage(fn) {
function handleMessage (line 381) | function handleMessage(event) {
function Peer (line 412) | function Peer(id, options) {
function DataConnection (line 710) | function DataConnection(peer, provider, options) {
function MediaConnection (line 893) | function MediaConnection(peer, provider, options) {
Condensed preview — 93 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (146K chars).
[
{
"path": ".editorconfig",
"chars": 367,
"preview": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# edit"
},
{
"path": ".ember-cli",
"chars": 280,
"preview": "{\n /**\n Ember CLI sends analytics information by default. The data is completely\n anonymous, but there are times "
},
{
"path": ".eslintignore",
"chars": 240,
"preview": "# unconventional js\n/blueprints/*/files/\n/vendor/\n\n# compiled output\n/dist/\n/tmp/\n\n# dependencies\n/bower_components/\n/no"
},
{
"path": ".eslintrc.js",
"chars": 1488,
"preview": "const eslintPluginNode = require('eslint-plugin-node');\n\nmodule.exports = {\n root: true,\n parser: 'babel-eslint',\n pa"
},
{
"path": ".gitignore",
"chars": 369,
"preview": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist/\n/tmp/\n\n# dependenci"
},
{
"path": ".prettierignore",
"chars": 139,
"preview": "##########\n# Common\n##########\n\n# Duh\n.git/\n\n# Third party\n/node_modules/\n/vendor/\n\n# Build products\n/dist/\n/tmp/\n/cover"
},
{
"path": ".template-lintrc.js",
"chars": 208,
"preview": "module.exports = {\n extends: 'octane',\n\n // TODO: enable these\n rules: {\n 'no-action': false,\n 'no-curly-compon"
},
{
"path": ".travis.yml",
"chars": 306,
"preview": "---\nlanguage: node_js\nnode_js:\n - \"12\"\n\ndist: xenial\n\naddons:\n chrome: stable\n\ncache:\n yarn: true\n\nenv:\n global:\n "
},
{
"path": ".watchmanconfig",
"chars": 49,
"preview": "{\n \"ignore_dirs\": [\n \"tmp\",\n \"dist\"\n ]\n}\n"
},
{
"path": "Dockerfile",
"chars": 190,
"preview": "FROM node:14-buster\nRUN mkdir -p /srv/app\nWORKDIR /srv/app\nCOPY package.json yarn.lock ./\nRUN yarn --frozen-lockfile --n"
},
{
"path": "LICENSE",
"chars": 1078,
"preview": "The MIT License\n\nCopyright (c) 2014-2024 Szymon Nowak\n\nPermission is hereby granted, free of charge, to any person obtai"
},
{
"path": "Procfile",
"chars": 20,
"preview": "web: node server.js\n"
},
{
"path": "Procfile.dev",
"chars": 48,
"preview": "server: node server.js\nweb: ember build --watch\n"
},
{
"path": "README.md",
"chars": 3323,
"preview": "<picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://user-images.githubusercontent.com/9873/20013775"
},
{
"path": "app/app.js",
"chars": 652,
"preview": "import Application from '@ember/application';\nimport Resolver from 'ember-resolver';\nimport loadInitializers from 'ember"
},
{
"path": "app/components/circular-progress.hbs",
"chars": 148,
"preview": "<svg width=\"76\" height=\"76\" viewport=\"0 0 76 76\" style={{this.style}}>\n <path class=\"break\" transform=\"translate(38, 38"
},
{
"path": "app/components/circular-progress.js",
"chars": 631,
"preview": "import Component from '@glimmer/component';\nimport { htmlSafe } from '@ember/template';\n\nconst COLORS = {\n blue: '0, 13"
},
{
"path": "app/components/file-field.js",
"chars": 655,
"preview": "import TextField from '@ember/component/text-field';\nimport $ from 'jquery';\n\nexport default TextField.extend({\n type: "
},
{
"path": "app/components/modal-dialog.js",
"chars": 257,
"preview": "import Component from '@ember/component';\n\nexport default Component.extend({\n actions: {\n close() {\n // This se"
},
{
"path": "app/components/peer-avatar.js",
"chars": 3433,
"preview": "import Component from '@ember/component';\nimport { alias } from '@ember/object/computed';\nimport { later } from '@ember/"
},
{
"path": "app/components/peer-widget.js",
"chars": 4019,
"preview": "import Component from '@ember/component';\nimport { computed } from '@ember/object';\nimport { alias, equal, gt } from '@e"
},
{
"path": "app/components/popover-confirm.js",
"chars": 617,
"preview": "import Component from '@ember/component';\nimport { computed } from '@ember/object';\n\nexport default Component.extend({\n "
},
{
"path": "app/components/room-url.js",
"chars": 463,
"preview": "import TextField from '@ember/component/text-field';\nimport $ from 'jquery';\n\nexport default TextField.extend({\n classN"
},
{
"path": "app/components/user-widget.js",
"chars": 147,
"preview": "import Component from '@ember/component';\n\nexport default Component.extend({\n classNames: ['peer'],\n classNameBindings"
},
{
"path": "app/controllers/application.js",
"chars": 821,
"preview": "import Controller from '@ember/controller';\nimport { inject as service } from '@ember/service';\nimport { v4 as uuidv4 } "
},
{
"path": "app/controllers/index.js",
"chars": 6497,
"preview": "import Controller, { inject as controller } from '@ember/controller';\nimport { alias } from '@ember/object/computed';\nim"
},
{
"path": "app/helpers/is-equal.js",
"chars": 145,
"preview": "import { helper as buildHelper } from '@ember/component/helper';\n\nexport default buildHelper(([leftSide, rightSide]) => "
},
{
"path": "app/index.html",
"chars": 1268,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"X-UA-Compatible\" content=\"I"
},
{
"path": "app/initializers/prerequisites.js",
"chars": 2365,
"preview": "/* jshint -W030 */\nimport $ from 'jquery';\nimport { Promise } from 'rsvp';\nimport config from 'sharedrop/config/environm"
},
{
"path": "app/models/peer.js",
"chars": 1491,
"preview": "import EmberObject, { observer } from '@ember/object';\nimport Evented, { on } from '@ember/object/evented';\n\nexport defa"
},
{
"path": "app/models/user.js",
"chars": 316,
"preview": "import Peer from './peer';\n\nconst User = Peer.extend({\n serialize() {\n const data = {\n uuid: this.uuid,\n p"
},
{
"path": "app/router.js",
"chars": 354,
"preview": "import EmberRouter from '@ember/routing/router';\nimport config from 'sharedrop/config/environment';\n\nexport default clas"
},
{
"path": "app/routes/application.js",
"chars": 445,
"preview": "import Route from '@ember/routing/route';\n\nexport default Route.extend({\n setupController(controller) {\n controller."
},
{
"path": "app/routes/error.js",
"chars": 314,
"preview": "import Route from '@ember/routing/route';\n\nexport default Route.extend({\n renderTemplate(controller, error) {\n const"
},
{
"path": "app/routes/index.js",
"chars": 2782,
"preview": "import Route from '@ember/routing/route';\nimport $ from 'jquery';\n\nimport Room from '../services/room';\n\nexport default "
},
{
"path": "app/routes/room.js",
"chars": 897,
"preview": "import IndexRoute from './index';\n\nexport default IndexRoute.extend({\n controllerName: 'index',\n\n model(params) {\n "
},
{
"path": "app/services/analytics.js",
"chars": 170,
"preview": "export default {\n trackEvent(name, parameters) {\n if (window.gtag && typeof window.gtag === 'function') {\n wind"
},
{
"path": "app/services/avatar.js",
"chars": 2302,
"preview": "import Service from '@ember/service';\nimport sample from 'lodash/sample';\nimport startCase from 'lodash/startCase';\n\ncon"
},
{
"path": "app/services/file.js",
"chars": 4206,
"preview": "import { Promise } from 'rsvp';\n\nconst File = function (options) {\n const self = this;\n\n this.name = options.name;\n t"
},
{
"path": "app/services/room.js",
"chars": 2509,
"preview": "import $ from 'jquery';\n\n// TODO: use Ember.Object.extend()\nconst Room = function (name, firebaseRef) {\n this._ref = fi"
},
{
"path": "app/services/web-rtc.js",
"chars": 8772,
"preview": "// TODO:\n// - provide TURN server config once it's possible to create rooms with custom names\n// - use Ember.Object.exte"
},
{
"path": "app/styles/app.sass",
"chars": 309,
"preview": "@import base/reset\n\n@import base/variables\n@import base/mixins\n@import base/element_defaults\n@import base/glyphicons_fil"
},
{
"path": "app/styles/base/_element_defaults.sass",
"chars": 378,
"preview": "*, *::before, *::after\n box-sizing: border-box\n\nhtml\n height: 100%\n font-family: $font-family\n font-size: 10px\n\nbody"
},
{
"path": "app/styles/base/_glyphicons_filetypes.sass",
"chars": 7411,
"preview": "/* GLYPHICONS FILETYPES 1.8 */\n\n@font-face\n font-family: \"Glyphicons Filetypes\"\n src: url(/assets/fonts/glyphicons/gly"
},
{
"path": "app/styles/base/_mixins.sass",
"chars": 437,
"preview": "=ellipsis\n overflow: hidden\n white-space: nowrap\n text-overflow: ellipsis\n\n=button_reset\n margin: 0\n padding: 0\n d"
},
{
"path": "app/styles/base/_reset.sass",
"chars": 967,
"preview": "a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,d"
},
{
"path": "app/styles/base/_variables.sass",
"chars": 75,
"preview": "$font-family: \"Helvetica Neue\", sans-serif\n\n$blue: #0088cc\n$green: #a4c540\n"
},
{
"path": "app/styles/layout/_content.sass",
"chars": 445,
"preview": ".l-content\n position: relative\n height: 100vh\n min-height: 600px\n\n.visually-hidden\n clip: rect(0 0 0 0)\n clip-path:"
},
{
"path": "app/styles/layout/_footer.sass",
"chars": 754,
"preview": ".l-footer\n position: fixed\n z-index: 200\n bottom: 0\n left: 0\n right: 0\n background-color: rgba(white,.6)\n text-al"
},
{
"path": "app/styles/layout/_header.sass",
"chars": 1165,
"preview": ".l-header\n .navbar\n position: fixed\n z-index: 10\n top: 0\n left: 0\n width: 100%\n height: 60px\n back"
},
{
"path": "app/styles/layout/_media.sass",
"chars": 1474,
"preview": "@media (max-height: 520px)\n .modal-body\n height: auto\n bottom: auto\n margin: 15px auto\n\n@media (max-width: 768"
},
{
"path": "app/styles/modules/_modal.sass",
"chars": 1332,
"preview": ".modal-overlay\n position: fixed\n z-index: 300\n top: 0\n bottom: 0\n left: 0\n right: 0\n background-color: rgba(black"
},
{
"path": "app/styles/modules/_modules.sass",
"chars": 975,
"preview": ".preloader\n position: absolute\n left: 0\n right: 0\n top: 0\n bottom: 0\n margin: auto\n width: 324px\n height: 56px\n "
},
{
"path": "app/styles/modules/_popover.sass",
"chars": 1280,
"preview": "$popover-border-color: #c0c0c0\n\n.popover\n position: absolute\n bottom: 100%\n left: 50%\n transform: translateX(-50%)\n "
},
{
"path": "app/styles/modules/_users.sass",
"chars": 3930,
"preview": "$user-size: 76px\n\n.user\n user-select: none\n\n .peer\n position: absolute\n left: 50%\n bottom: 300px\n width: $"
},
{
"path": "app/templates/about-app.hbs",
"chars": 2330,
"preview": "{{#modal-dialog onClose=(action \"closeModal\" target=currentRoute)}}\n <h2 class=\"logo\"><span>ShareDrop</span></h2>\n <h3"
},
{
"path": "app/templates/about-room.hbs",
"chars": 896,
"preview": "{{#modal-dialog onClose=(action \"closeModal\" target=currentRoute)}}\n <h2 class=\"logo\"><span>ShareDrop</span></h2>\n <h3"
},
{
"path": "app/templates/about-you.hbs",
"chars": 94,
"preview": "ShareDrop lets you share files with others.\nOther people will see you as <b>{{you.label}}</b>."
},
{
"path": "app/templates/application.hbs",
"chars": 1402,
"preview": "<header class=\"l-header\">\n <nav class=\"navbar\">\n <h1 class=\"logo\">\n <span class=\"logo-title\">ShareDrop</span>\n "
},
{
"path": "app/templates/components/modal-dialog.hbs",
"chars": 199,
"preview": "{{! template-lint-disable no-invalid-interactive }}\n<div class=\"modal-overlay\" {{action \"close\"}}></div>\n<div class=\"mod"
},
{
"path": "app/templates/components/peer-widget.hbs",
"chars": 2502,
"preview": "{{! Sender related messages }}\n{{#if hasSelectedFile}}\n {{#popover-confirm\n onConfirm=(action \"sendFileTransferInqui"
},
{
"path": "app/templates/components/popover-confirm.hbs",
"chars": 454,
"preview": "<div class=\"popover\">\n <div class=\"popover-body\">\n <div class=\"popover-icon\">\n <i class={{iconClass}}></i>\n "
},
{
"path": "app/templates/components/user-widget.hbs",
"chars": 259,
"preview": "<div class=\"avatar\">\n <img class=\"gravatar\" src={{user.avatarUrl}} alt={{users.label}} title=\"peer id: {{user.uuid}}\">\n"
},
{
"path": "app/templates/errors/browser-unsupported.hbs",
"chars": 196,
"preview": "<div class=\"error\">\n <p>We're really sorry, but your browser is not supported.<br>Please use the latest desktop or Andr"
},
{
"path": "app/templates/errors/filesystem-unavailable.hbs",
"chars": 211,
"preview": "<div class=\"error\">\n <p>Uh oh. Looks like there's some issue and we won't be able<br>to save your files.</p>\n <p>If yo"
},
{
"path": "app/templates/errors/popovers/connection-failed.hbs",
"chars": 72,
"preview": "It was not possible to establish direct connection with the other peer.\n"
},
{
"path": "app/templates/errors/popovers/multiple-files.hbs",
"chars": 134,
"preview": "The files you have selected exceed the maximum allowed size of 200MB\n<br><br>\nTIP: You can send single files without siz"
},
{
"path": "app/templates/index.hbs",
"chars": 874,
"preview": "<main class=\"l-content\">\n <div class=\"user others\">\n {{#each model as |peer|}}\n {{peer-widget peer=peer hasCust"
},
{
"path": "config/dotenv.js",
"chars": 233,
"preview": "module.exports = function () {\n return {\n clientAllowedKeys: ['FIREBASE_URL'],\n // Fail build when there is missi"
},
{
"path": "config/environment.js",
"chars": 1302,
"preview": "module.exports = function (environment) {\n const ENV = {\n modulePrefix: 'sharedrop',\n environment,\n rootURL: '"
},
{
"path": "config/optional-features.json",
"chars": 153,
"preview": "{\n \"application-template-wrapper\": false,\n \"default-async-observers\": true,\n \"jquery-integration\": true,\n \"template-"
},
{
"path": "config/targets.js",
"chars": 193,
"preview": "const browsers = [\n 'last 2 Chrome versions',\n 'last 2 Firefox versions',\n 'last 2 Safari versions',\n 'last 2 iOS ve"
},
{
"path": "ember-cli-build.js",
"chars": 1266,
"preview": "/* eslint-env node */\nconst EmberApp = require('ember-cli/lib/broccoli/ember-app');\n\nmodule.exports = function (defaults"
},
{
"path": "firebase_rules.json",
"chars": 1051,
"preview": "{\n \"rules\": {\n \"rooms\": {\n \"$roomid\": {\n // You can see people in the room only if y"
},
{
"path": "lib/google-analytics/index.js",
"chars": 572,
"preview": "const { name } = require('./package');\n\nmodule.exports = {\n name,\n\n isDevelopingAddon() {\n return true;\n },\n\n con"
},
{
"path": "lib/google-analytics/package.json",
"chars": 72,
"preview": "{\n \"name\": \"google-analytics\",\n \"keywords\": [\n \"ember-addon\"\n ]\n}\n"
},
{
"path": "newrelic.js",
"chars": 535,
"preview": "/**\n * New Relic agent configuration.\n *\n * See lib/config.defaults.js in the agent distribution for a more complete\n * "
},
{
"path": "package.json",
"chars": 2978,
"preview": "{\n \"name\": \"sharedrop\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"description\": \"P2P file sharing\",\n \"license\": \"MIT"
},
{
"path": "prettier.config.js",
"chars": 255,
"preview": "// https://prettier.io/docs/en/options.html\n\nmodule.exports = {\n printWidth: 80,\n tabWidth: 2,\n useTabs: false,\n sem"
},
{
"path": "public/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "public/.well-known/brave-rewards-verification.txt",
"chars": 147,
"preview": "This is a Brave Rewards publisher verification file.\n\nDomain: sharedrop.io\nToken: d463c371edd92de3a09b0ccf1ce8143b9ee7f8"
},
{
"path": "public/crossdomain.xml",
"chars": 585,
"preview": "<?xml version=\"1.0\"?>\n<!DOCTYPE cross-domain-policy SYSTEM \"http://www.adobe.com/xml/dtds/cross-domain-policy.dtd\">\n<cro"
},
{
"path": "public/robots.txt",
"chars": 51,
"preview": "# http://www.robotstxt.org\nUser-agent: *\nDisallow:\n"
},
{
"path": "server.js",
"chars": 2446,
"preview": "/* eslint-env node */\n\nif (process.env.NODE_ENV === 'production') {\n // eslint-disable-next-line global-require\n requi"
},
{
"path": "sharedrop.crx",
"chars": 318,
"preview": "{\n \"name\": \"ShareDrop\",\n \"description\": \"Simple file sharing\",\n \"version\": \"1\",\n \"app\": {\n \"urls\": [\n \"https"
},
{
"path": "testem.js",
"chars": 574,
"preview": "module.exports = {\n test_page: 'tests/index.html?hidepassed',\n disable_watching: true,\n launch_in_ci: ['Chrome'],\n l"
},
{
"path": "tests/helpers/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "tests/index.html",
"chars": 1060,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n "
},
{
"path": "tests/integration/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "tests/test-helper.js",
"chars": 255,
"preview": "/* eslint */\nimport { setApplication } from '@ember/test-helpers';\nimport { start } from 'ember-qunit';\nimport Applicati"
},
{
"path": "tests/unit/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "vendor/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "vendor/peer.js",
"chars": 35176,
"preview": "/*! peerjs.js build:0.3.7, development. Copyright(c) 2013 Michelle Bu <michelle@michellebu.com> */\n(function(exports){\n/"
}
]
About this extraction
This page contains the full source code of the ShareDropio/sharedrop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 93 files (131.4 KB), approximately 37.8k tokens, and a symbol index with 86 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.