Handle file transfers inside CraftOS (#1190)

- Add a new file_transfer event. This has the signature
   "file_transfer", TransferredFiles.

   TransferredFiles has a single method getFiles(), which returns a list
   of all transferred files.

 - Add a new "import" program which waits for a file_transfer event and
   writes files to the current directory.

 - If a file_transfer event is not handled (i.e. its getFiles() method
   is not called) within 5 seconds on the client, we display a toast
   informing the user on how to upload a file.
This commit is contained in:
Jonathan Coates 2022-10-29 12:01:23 +01:00 committed by GitHub
parent 1f910ee2ba
commit 97387556fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 748 additions and 222 deletions

View File

@ -0,0 +1,41 @@
---
module: [kind=event] file_transfer
---
The @{file_transfer} event is queued when a user drags-and-drops a file on an open computer.
This event contains a single argument, that in turn has a single method @{TransferredFiles.getFiles|getFiles}. This
returns the list of files that are being transferred. Each file is a @{fs.BinaryReadHandle|binary file handle} with an
additional @{TransferredFile.getName|getName} method.
## Return values
1. @{string}: The event name
2. @{TransferredFiles}: The list of transferred files.
## Example
Waits for a user to drop files on top of the computer, then prints the list of files and the size of each file.
```lua
local _, files = os.pullEvent("file_transfer")
for _, file in ipairs(files.getFiles()) do
-- Seek to the end of the file to get its size, then go back to the beginning.
local size = file.seek("end")
file.seek("set", 0)
print(file.getName() .. " " .. file.getSize())
end
```
## Example
Save each transferred file to the computer's storage.
```lua
local _, files = os.pullEvent("file_transfer")
for _, file in ipairs(files.getFiles()) do
local handle = fs.open(file.getName(), "wb")
handle.write(file.readAll())
handle.close()
file.close()
end
```

View File

@ -18,12 +18,12 @@ jqwik = "1.7.0"
junit = "5.9.1"
# Build tools
cctJavadoc = "1.5.1"
cctJavadoc = "1.5.2"
checkstyle = "8.25" # There's a reason we're pinned on an ancient version, but I can't remember what it is.
curseForgeGradle = "1.0.11"
forgeGradle = "5.1.+"
githubRelease = "2.2.12"
illuaminate = "0.1.0-3-g0f40379"
illuaminate = "0.1.0-7-g2a5a89c"
librarian = "1.+"
minotaur = "2.+"
mixinGradle = "0.7.+"

View File

@ -82,6 +82,8 @@ public final class ComputerCraft
public static int monitorWidth = 8;
public static int monitorHeight = 6;
public static int uploadNagDelay = 5;
public static final class TurtleUpgrades
{
public static TurtleModem wirelessModemNormal;

View File

@ -17,30 +17,35 @@
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.server.ContinueUploadMessage;
import dan200.computercraft.shared.network.server.UploadFileMessage;
import net.minecraft.client.gui.screen.inventory.ContainerScreen;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.util.Util;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.StringTextComponent;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.util.text.TranslationTextComponent;
import org.lwjgl.glfw.GLFW;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
public abstract class ComputerScreenBase<T extends ContainerComputerBase> extends ContainerScreen<T>
{
private static final ITextComponent OK = new TranslationTextComponent( "gui.ok" );
private static final ITextComponent CANCEL = new TranslationTextComponent( "gui.cancel" );
private static final ITextComponent OVERWRITE = new TranslationTextComponent( "gui.computercraft.upload.overwrite_button" );
private static final ITextComponent NO_RESPONSE_TITLE = new TranslationTextComponent( "gui.computercraft.upload.no_response" );
private static final ITextComponent NO_RESPONSE_MSG = new TranslationTextComponent( "gui.computercraft.upload.no_response.msg",
new StringTextComponent( "import" ).withStyle( TextFormatting.DARK_GRAY ) );
protected WidgetTerminal terminal;
protected Terminal terminalData;
@ -49,11 +54,15 @@ public abstract class ComputerScreenBase<T extends ContainerComputerBase> extend
protected final int sidebarYOffset;
private long uploadNagDeadline = Long.MAX_VALUE;
private final ItemStack displayStack;
public ComputerScreenBase( T container, PlayerInventory player, ITextComponent title, int sidebarYOffset )
{
super( container, player, title );
terminalData = container.getTerminal();
family = container.getFamily();
displayStack = container.getDisplayStack();
input = new ClientInputHandler( menu );
this.sidebarYOffset = sidebarYOffset;
}
@ -83,6 +92,13 @@ public void tick()
{
super.tick();
terminal.update();
if( uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline )
{
new ItemToast( minecraft, displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN )
.showOrReplace( minecraft.getToasts() );
uploadNagDeadline = Long.MAX_VALUE;
}
}
@Override
@ -194,41 +210,29 @@ public void onFilesDrop( @Nonnull List<Path> files )
if( toUpload.size() > 0 ) UploadFileMessage.send( menu, toUpload, NetworkHandler::sendToServer );
}
public void uploadResult( UploadResult result, ITextComponent message )
public void uploadResult( UploadResult result, @Nullable ITextComponent message )
{
switch( result )
{
case SUCCESS:
alert( UploadResult.SUCCESS_TITLE, message );
case QUEUED:
{
if( ComputerCraft.uploadNagDelay > 0 )
{
uploadNagDeadline = Util.getNanos() + TimeUnit.SECONDS.toNanos( ComputerCraft.uploadNagDelay );
}
break;
}
case CONSUMED:
{
uploadNagDeadline = Long.MAX_VALUE;
break;
}
case ERROR:
alert( UploadResult.FAILED_TITLE, message );
break;
case CONFIRM_OVERWRITE:
OptionScreen.show(
minecraft, UploadResult.UPLOAD_OVERWRITE, message,
Arrays.asList(
OptionScreen.newButton( CANCEL, b -> cancelUpload() ),
OptionScreen.newButton( OVERWRITE, b -> continueUpload() )
),
this::cancelUpload
);
break;
}
}
private void continueUpload()
{
if( minecraft.screen instanceof OptionScreen ) ((OptionScreen) minecraft.screen).disable();
NetworkHandler.sendToServer( new ContinueUploadMessage( menu, true ) );
}
private void cancelUpload()
{
minecraft.setScreen( this );
NetworkHandler.sendToServer( new ContinueUploadMessage( menu, false ) );
}
private void alert( ITextComponent title, ITextComponent message )
{
OptionScreen.show( minecraft, title, message,

View File

@ -0,0 +1,150 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client.gui;
import com.mojang.blaze3d.matrix.MatrixStack;
import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.toasts.IToast;
import net.minecraft.client.gui.toasts.ToastGui;
import net.minecraft.item.ItemStack;
import net.minecraft.util.IReorderingProcessor;
import net.minecraft.util.text.ITextComponent;
import javax.annotation.Nonnull;
import java.util.List;
/**
* A {@link IToast} implementation which displays an arbitrary message along with an optional {@link ItemStack}.
*/
public class ItemToast implements IToast
{
public static final Object TRANSFER_NO_RESPONSE_TOKEN = new Object();
private static final long DISPLAY_TIME = 7000L;
private static final int MAX_LINE_SIZE = 200;
private static final int IMAGE_SIZE = 16;
private static final int LINE_SPACING = 10;
private static final int MARGIN = 8;
private final ItemStack stack;
private final ITextComponent title;
private final List<IReorderingProcessor> message;
private final Object token;
private final int width;
private boolean isNew = true;
private long firstDisplay;
public ItemToast( Minecraft minecraft, ItemStack stack, ITextComponent title, ITextComponent message, Object token )
{
this.stack = stack;
this.title = title;
this.token = token;
FontRenderer font = minecraft.font;
this.message = font.split( message, MAX_LINE_SIZE );
width = Math.max( MAX_LINE_SIZE, this.message.stream().mapToInt( font::width ).max().orElse( MAX_LINE_SIZE ) ) + MARGIN * 3 + IMAGE_SIZE;
}
public void showOrReplace( ToastGui toasts )
{
ItemToast existing = toasts.getToast( ItemToast.class, getToken() );
if( existing != null )
{
existing.isNew = true;
}
else
{
toasts.addToast( this );
}
}
@Override
public int width()
{
return width;
}
@Override
public int height()
{
return MARGIN * 2 + LINE_SPACING + message.size() * LINE_SPACING;
}
@Nonnull
@Override
public Object getToken()
{
return token;
}
@Nonnull
@Override
public Visibility render( @Nonnull MatrixStack transform, @Nonnull ToastGui component, long time )
{
if( isNew )
{
firstDisplay = time;
isNew = false;
}
component.getMinecraft().getTextureManager().bind( TEXTURE );
RenderSystem.color3f( 1.0F, 1.0F, 1.0F );
if( width == 160 && message.size() <= 1 )
{
component.blit( transform, 0, 0, 0, 64, width, height() );
}
else
{
int height = height();
int bottom = Math.min( 4, height - 28 );
renderBackgroundRow( transform, component, width, 0, 0, 28 );
for( int i = 28; i < height - bottom; i += 10 )
{
renderBackgroundRow( transform, component, width, 16, i, Math.min( 16, height - i - bottom ) );
}
renderBackgroundRow( transform, component, width, 32 - bottom, height - bottom, bottom );
}
int textX = MARGIN;
if( !stack.isEmpty() )
{
textX += MARGIN + IMAGE_SIZE;
component.getMinecraft().getItemRenderer().renderAndDecorateFakeItem( stack, MARGIN, MARGIN + height() / 2 - IMAGE_SIZE );
}
component.getMinecraft().font.draw( transform, title, textX, MARGIN, 0xff500050 );
for( int i = 0; i < message.size(); ++i )
{
component.getMinecraft().font.draw( transform, message.get( i ), textX, (float) (LINE_SPACING + (i + 1) * LINE_SPACING), 0xff000000 );
}
return time - firstDisplay < DISPLAY_TIME ? Visibility.SHOW : Visibility.HIDE;
}
private static void renderBackgroundRow( MatrixStack transform, ToastGui component, int x, int u, int y, int height )
{
int leftOffset = 5;
int rightOffset = Math.min( 60, x - leftOffset );
component.blit( transform, 0, y, 0, 32 + u, leftOffset, height );
for( int k = leftOffset; k < x - rightOffset; k += 64 )
{
component.blit( transform, k, y, 32, 32 + u, Math.min( 64, x - k - rightOffset ), height );
}
component.blit( transform, x - rightOffset, y, 160 - rightOffset, 32 + u, rightOffset, height );
}
}

View File

@ -0,0 +1,99 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.handles;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.SeekableByteChannel;
import java.util.Objects;
/**
* A seekable, readable byte channel which is backed by a {@link ByteBuffer}.
*/
public class ByteBufferChannel implements SeekableByteChannel
{
private boolean closed = false;
private int position = 0;
private final ByteBuffer backing;
public ByteBufferChannel( ByteBuffer backing )
{
this.backing = backing;
}
@Override
public int read( ByteBuffer destination ) throws ClosedChannelException
{
if( closed ) throw new ClosedChannelException();
Objects.requireNonNull( destination, "destination" );
if( position >= backing.limit() ) return -1;
int remaining = Math.min( backing.limit() - position, destination.remaining() );
// TODO: Switch to Java 17 methods on 1.18.x
ByteBuffer slice = backing.slice();
slice.position( position );
slice.limit( position + remaining );
destination.put( slice );
position += remaining;
return remaining;
}
@Override
public int write( ByteBuffer src ) throws ClosedChannelException
{
if( closed ) throw new ClosedChannelException();
throw new NonWritableChannelException();
}
@Override
public long position() throws ClosedChannelException
{
if( closed ) throw new ClosedChannelException();
return position;
}
@Override
public SeekableByteChannel position( long newPosition ) throws ClosedChannelException
{
if( closed ) throw new ClosedChannelException();
if( newPosition < 0 || newPosition > Integer.MAX_VALUE )
{
throw new IllegalArgumentException( "Position out of bounds" );
}
position = (int) newPosition;
return this;
}
@Override
public long size() throws ClosedChannelException
{
if( closed ) throw new ClosedChannelException();
return backing.limit();
}
@Override
public SeekableByteChannel truncate( long size ) throws ClosedChannelException
{
if( closed ) throw new ClosedChannelException();
throw new NonWritableChannelException();
}
@Override
public boolean isOpen()
{
return !closed;
}
@Override
public void close()
{
closed = true;
}
}

View File

@ -293,6 +293,7 @@ protected Void prepare( @Nonnull IResourceManager manager, @Nonnull IProfiler pr
try
{
for( ResourceMount mount : MOUNT_CACHE.values() ) mount.load( manager );
CONTENTS_CACHE.invalidateAll();
}
finally
{

View File

@ -17,7 +17,6 @@
import dan200.computercraft.core.apis.http.options.AddressRuleConfig;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.common.ForgeConfigSpec.Builder;
import net.minecraftforge.common.ForgeConfigSpec.ConfigValue;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModLoadingContext;
@ -86,6 +85,7 @@ public final class Config
private static final ConfigValue<MonitorRenderer> monitorRenderer;
private static final ConfigValue<Integer> monitorDistance;
private static final ConfigValue<Integer> uploadNagDelay;
private static final ForgeConfigSpec serverSpec;
private static final ForgeConfigSpec clientSpec;
@ -94,7 +94,7 @@ private Config() {}
static
{
Builder builder = new Builder();
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
{ // General computers
computerSpaceLimit = builder
@ -282,13 +282,17 @@ private Config() {}
serverSpec = builder.build();
Builder clientBuilder = new Builder();
ForgeConfigSpec.Builder clientBuilder = new ForgeConfigSpec.Builder();
monitorRenderer = clientBuilder
.comment( "The renderer to use for monitors. Generally this should be kept at \"best\" - if\nmonitors have performance issues, you may wish to experiment with alternative\nrenderers." )
.defineEnum( "monitor_renderer", MonitorRenderer.BEST );
monitorDistance = clientBuilder
.comment( "The maximum distance monitors will render at. This defaults to the standard tile\nentity limit, but may be extended if you wish to build larger monitors." )
.defineInRange( "monitor_distance", 64, 16, 1024 );
uploadNagDelay = clientBuilder
.comment( "The delay in seconds after which we'll notify about unhandled imports. Set to 0 to disable." )
.defineInRange( "upload_nag_delay", ComputerCraft.uploadNagDelay, 0, 60 );
clientSpec = clientBuilder.build();
}
@ -357,6 +361,7 @@ public static void sync()
// Client
ComputerCraft.monitorRenderer = monitorRenderer.get();
ComputerCraft.monitorDistanceSq = monitorDistance.get() * monitorDistance.get();
ComputerCraft.uploadNagDelay = uploadNagDelay.get();
}
@SubscribeEvent

View File

@ -28,6 +28,7 @@
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.inventory.container.Container;
import net.minecraft.inventory.container.INamedContainerProvider;
import net.minecraft.item.ItemStack;
import net.minecraft.network.play.server.SPlayerPositionLookPacket;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.text.IFormattableTextComponent;
@ -225,7 +226,7 @@ else if( b.getWorld() == world )
.executes( context -> {
ServerPlayerEntity player = context.getSource().getPlayerOrException();
ServerComputer computer = getComputerArgument( context, "computer" );
new ComputerContainerData( computer ).open( player, new INamedContainerProvider()
new ComputerContainerData( computer, ItemStack.EMPTY ).open( player, new INamedContainerProvider()
{
@Nonnull
@Override

View File

@ -146,7 +146,11 @@ else if( !player.isCrouching() )
{
ServerComputer computer = createServerComputer();
computer.turnOn();
new ComputerContainerData( computer ).open( player, this );
ItemStack stack = getBlockState().getBlock() instanceof BlockComputerBase<?>
? ((BlockComputerBase<?>) getBlockState().getBlock()).getItem( this )
: ItemStack.EMPTY;
new ComputerContainerData( computer, stack ).open( player, this );
}
return ActionResultType.SUCCESS;
}

View File

@ -9,6 +9,7 @@
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
import dan200.computercraft.shared.computer.menu.ServerInputState;
import dan200.computercraft.shared.network.client.TerminalState;
import dan200.computercraft.shared.network.container.ComputerContainerData;
@ -16,6 +17,7 @@
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.inventory.container.Container;
import net.minecraft.inventory.container.ContainerType;
import net.minecraft.item.ItemStack;
import net.minecraft.util.IIntArray;
import net.minecraft.util.IntArray;
@ -30,10 +32,12 @@ public abstract class ContainerComputerBase extends Container implements Compute
private final IIntArray data;
private final @Nullable ServerComputer computer;
private final @Nullable ServerInputState input;
private final @Nullable ServerInputState<ContainerComputerBase> input;
private final @Nullable Terminal terminal;
private final ItemStack displayStack;
public ContainerComputerBase(
ContainerType<? extends ContainerComputerBase> type, int id, Predicate<PlayerEntity> canUse,
ComputerFamily family, @Nullable ServerComputer computer, @Nullable ComputerContainerData containerData
@ -46,8 +50,9 @@ public ContainerComputerBase(
addDataSlots( data );
this.computer = computer;
input = computer == null ? null : new ServerInputState( this );
input = computer == null ? null : new ServerInputState<>( this );
terminal = containerData == null ? null : containerData.terminal().create();
displayStack = containerData == null ? null : containerData.displayStack();
}
@Override
@ -75,7 +80,7 @@ public ServerComputer getComputer()
}
@Override
public ServerInputState getInput()
public ServerInputHandler getInput()
{
if( input == null ) throw new UnsupportedOperationException( "Cannot access server computer on the client" );
return input;
@ -106,4 +111,15 @@ public void removed( @Nonnull PlayerEntity player )
super.removed( player );
if( input != null ) input.close();
}
/**
* Get the stack associated with this container.
*
* @return The current stack.
*/
@Nonnull
public ItemStack getDisplayStack()
{
return displayStack;
}
}

View File

@ -46,12 +46,4 @@ public interface ServerInputHandler extends InputHandler
* @param uploadId The unique ID of this upload.
*/
void finishUpload( ServerPlayerEntity uploader, UUID uploadId );
/**
* Continue an upload.
*
* @param uploader The player uploading files.
* @param overwrite Whether the files should be overwritten or not.
*/
void confirmUpload( ServerPlayerEntity uploader, boolean overwrite );
}

View File

@ -6,13 +6,8 @@
package dan200.computercraft.shared.computer.menu;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.filesystem.FileSystemWrapper;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.upload.FileSlice;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.computer.upload.*;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.UploadResultMessage;
@ -20,27 +15,24 @@
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.inventory.container.Container;
import net.minecraft.util.text.TranslationTextComponent;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* The default concrete implementation of {@link ServerInputHandler}.
* <p>
* This keeps track of the current key and mouse state, and releases them when the container is closed.
*
* @param <T> The type of container this server input belongs to.
*/
public class ServerInputState implements ServerInputHandler
public class ServerInputState<T extends Container & ComputerMenu> implements ServerInputHandler
{
private static final String LIST_PREFIX = "\n \u2022 ";
private final ComputerMenu owner;
private final T owner;
private final IntSet keysDown = new IntOpenHashSet( 4 );
private int lastMouseX;
@ -50,7 +42,7 @@ public class ServerInputState implements ServerInputHandler
private @Nullable UUID toUploadId;
private @Nullable List<FileUpload> toUpload;
public ServerInputState( ComputerMenu owner )
public ServerInputState( T owner )
{
this.owner = owner;
}
@ -160,91 +152,31 @@ public void finishUpload( ServerPlayerEntity uploader, UUID uploadId )
return;
}
NetworkMessage message = finishUpload( false );
NetworkMessage message = finishUpload( uploader );
NetworkHandler.sendToPlayer( uploader, message );
}
@Override
public void confirmUpload( ServerPlayerEntity uploader, boolean overwrite )
{
if( toUploadId == null || toUpload == null || toUpload.isEmpty() )
{
ComputerCraft.log.warn( "Invalid finishUpload call, skipping." );
return;
}
NetworkMessage message = finishUpload( true );
NetworkHandler.sendToPlayer( uploader, message );
}
private UploadResultMessage finishUpload( boolean forceOverwrite )
private UploadResultMessage finishUpload( ServerPlayerEntity player )
{
ServerComputer computer = owner.getComputer();
if( toUpload == null ) return UploadResultMessage.COMPUTER_OFF;
FileSystem fs = computer.getComputer().getAPIEnvironment().getFileSystem();
if( toUpload == null )
{
return UploadResultMessage.error( owner, UploadResult.COMPUTER_OFF_MSG );
}
for( FileUpload upload : toUpload )
{
if( !upload.checksumMatches() )
{
ComputerCraft.log.warn( "Checksum failed to match for {}.", upload.getName() );
return new UploadResultMessage( UploadResult.ERROR, new TranslationTextComponent( "gui.computercraft.upload.failed.corrupted" ) );
return UploadResultMessage.error( owner, new TranslationTextComponent( "gui.computercraft.upload.failed.corrupted" ) );
}
}
try
{
List<String> overwrite = new ArrayList<>();
List<FileUpload> files = toUpload;
toUpload = null;
for( FileUpload upload : files )
{
if( !fs.exists( upload.getName() ) ) continue;
if( fs.isDir( upload.getName() ) )
{
return new UploadResultMessage(
UploadResult.ERROR,
new TranslationTextComponent( "gui.computercraft.upload.failed.overwrite_dir", upload.getName() )
);
}
overwrite.add( upload.getName() );
}
if( !overwrite.isEmpty() && !forceOverwrite )
{
StringJoiner joiner = new StringJoiner( LIST_PREFIX, LIST_PREFIX, "" );
for( String value : overwrite ) joiner.add( value );
toUpload = files;
return new UploadResultMessage(
UploadResult.CONFIRM_OVERWRITE,
new TranslationTextComponent( "gui.computercraft.upload.overwrite.detail", joiner.toString() )
);
}
long availableSpace = fs.getFreeSpace( "/" );
long neededSpace = 0;
for( FileUpload upload : files ) neededSpace += Math.max( 512, upload.getBytes().remaining() );
if( neededSpace > availableSpace ) return UploadResultMessage.OUT_OF_SPACE;
for( FileUpload file : files )
{
try( FileSystemWrapper<WritableByteChannel> channel = fs.openForWrite( file.getName(), false, Function.identity() ) )
{
channel.get().write( file.getBytes() );
}
}
return new UploadResultMessage(
UploadResult.SUCCESS, new TranslationTextComponent( "gui.computercraft.upload.success.msg", files.size() )
);
}
catch( FileSystemException | IOException e )
{
ComputerCraft.log.error( "Error uploading files", e );
return new UploadResultMessage( UploadResult.ERROR, new TranslationTextComponent( "gui.computercraft.upload.failed.generic", e.getMessage() ) );
}
computer.queueEvent( "file_transfer", new Object[] {
new TransferredFiles( player, owner, toUpload.stream().map( x -> new TransferredFile( x.getName(), x.getBytes() ) ).collect( Collectors.toList() ) ),
} );
return UploadResultMessage.queued( owner );
}
public void close()

View File

@ -0,0 +1,53 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.computer.upload;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
import dan200.computercraft.core.asm.ObjectSource;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Optional;
/**
* A binary file handle that has been transferred to this computer.
* <p>
* This inherits all methods of {@link BinaryReadableHandle binary file handles}, meaning you can use the standard
* {@link BinaryReadableHandle#read(Optional) read functions} to access the contents of the file.
*
* @cc.module [kind=event] file_transfer.TransferredFile
* @see BinaryReadableHandle
*/
public class TransferredFile implements ObjectSource
{
private final String name;
private final BinaryReadableHandle handle;
public TransferredFile( String name, ByteBuffer contents )
{
this.name = name;
handle = BinaryReadableHandle.of( new ByteBufferChannel( contents ) );
}
/**
* Get the name of this file being transferred.
*
* @return The file's name.
*/
@LuaFunction
public final String getName()
{
return name;
}
@Override
public Iterable<Object> getExtra()
{
return Collections.singleton( handle );
}
}

View File

@ -0,0 +1,58 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.computer.upload;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.client.UploadResultMessage;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.inventory.container.Container;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A list of files that have been transferred to this computer.
*
* @cc.module [kind=event] file_transfer.TransferredFiles
*/
public class TransferredFiles
{
private final ServerPlayerEntity player;
private final Container container;
private final AtomicBoolean consumed = new AtomicBoolean( false );
private final List<TransferredFile> files;
public TransferredFiles( ServerPlayerEntity player, Container container, List<TransferredFile> files )
{
this.player = player;
this.container = container;
this.files = files;
}
/**
* All the files that are being transferred to this computer.
*
* @return The list of files.
*/
@LuaFunction
public final List<TransferredFile> getFiles()
{
consumed();
return files;
}
private void consumed()
{
if( consumed.getAndSet( true ) ) return;
if( player.isAlive() && player.containerMenu == container )
{
NetworkHandler.sendToPlayer( player, UploadResultMessage.consumed( container ) );
}
}
}

View File

@ -10,16 +10,13 @@
public enum UploadResult
{
SUCCESS,
ERROR,
CONFIRM_OVERWRITE;
QUEUED,
CONSUMED,
ERROR;
public static final ITextComponent SUCCESS_TITLE = new TranslationTextComponent( "gui.computercraft.upload.success" );
public static final ITextComponent FAILED_TITLE = new TranslationTextComponent( "gui.computercraft.upload.failed" );
public static final ITextComponent COMPUTER_OFF_MSG = new TranslationTextComponent( "gui.computercraft.upload.failed.computer_off" );
public static final ITextComponent OUT_OF_SPACE_MSG = new TranslationTextComponent( "gui.computercraft.upload.failed.out_of_space" );
public static final ITextComponent TOO_MUCH_MSG = new TranslationTextComponent( "gui.computercraft.upload.failed.too_much" );
public static final ITextComponent UPLOAD_OVERWRITE = new TranslationTextComponent( "gui.computercraft.upload.overwrite" );
}

View File

@ -50,7 +50,6 @@ public static void setup()
registerMainThread( 2, NetworkDirection.PLAY_TO_SERVER, KeyEventServerMessage.class, KeyEventServerMessage::new );
registerMainThread( 3, NetworkDirection.PLAY_TO_SERVER, MouseEventServerMessage.class, MouseEventServerMessage::new );
registerMainThread( 4, NetworkDirection.PLAY_TO_SERVER, UploadFileMessage.class, UploadFileMessage::new );
registerMainThread( 5, NetworkDirection.PLAY_TO_SERVER, ContinueUploadMessage.class, ContinueUploadMessage::new );
// Client messages
registerMainThread( 10, NetworkDirection.PLAY_TO_CLIENT, ChatTableClientMessage.class, ChatTableClientMessage::new );

View File

@ -11,37 +11,55 @@
import dan200.computercraft.shared.network.NetworkMessage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.inventory.container.Container;
import net.minecraft.network.PacketBuffer;
import net.minecraft.util.text.ITextComponent;
import net.minecraftforge.fml.network.NetworkEvent;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class UploadResultMessage implements NetworkMessage
{
public static final UploadResultMessage COMPUTER_OFF = new UploadResultMessage( UploadResult.ERROR, UploadResult.COMPUTER_OFF_MSG );
public static final UploadResultMessage OUT_OF_SPACE = new UploadResultMessage( UploadResult.ERROR, UploadResult.OUT_OF_SPACE_MSG );
private final int containerId;
private final UploadResult result;
private final ITextComponent message;
private final ITextComponent errorMessage;
public UploadResultMessage( UploadResult result, ITextComponent message )
private UploadResultMessage( Container container, UploadResult result, @Nullable ITextComponent errorMessage )
{
containerId = container.containerId;
this.result = result;
this.message = message;
this.errorMessage = errorMessage;
}
public static UploadResultMessage queued( Container container )
{
return new UploadResultMessage( container, UploadResult.QUEUED, null );
}
public static UploadResultMessage consumed( Container container )
{
return new UploadResultMessage( container, UploadResult.CONSUMED, null );
}
public static UploadResultMessage error( Container container, ITextComponent errorMessage )
{
return new UploadResultMessage( container, UploadResult.ERROR, errorMessage );
}
public UploadResultMessage( @Nonnull PacketBuffer buf )
{
containerId = buf.readVarInt();
result = buf.readEnum( UploadResult.class );
message = buf.readComponent();
errorMessage = result == UploadResult.ERROR ? buf.readComponent() : null;
}
@Override
public void toBytes( @Nonnull PacketBuffer buf )
{
buf.writeVarInt( containerId );
buf.writeEnum( result );
buf.writeComponent( message );
if( result == UploadResult.ERROR ) buf.writeComponent( errorMessage );
}
@Override
@ -50,9 +68,9 @@ public void handle( NetworkEvent.Context context )
Minecraft minecraft = Minecraft.getInstance();
Screen screen = OptionScreen.unwrap( minecraft.screen );
if( screen instanceof ComputerScreenBase<?> )
if( screen instanceof ComputerScreenBase<?> && ((ComputerScreenBase<?>) screen).getMenu().containerId == containerId )
{
((ComputerScreenBase<?>) screen).uploadResult( result, message );
((ComputerScreenBase<?>) screen).uploadResult( result, errorMessage );
}
}
}

View File

@ -8,23 +8,29 @@
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.network.client.TerminalState;
import net.minecraft.item.ItemStack;
import net.minecraft.network.PacketBuffer;
import javax.annotation.Nonnull;
public class ComputerContainerData implements ContainerData
{
private final ComputerFamily family;
private final TerminalState terminal;
private final ItemStack displayStack;
public ComputerContainerData( ServerComputer computer )
public ComputerContainerData( ServerComputer computer, @Nonnull ItemStack displayStack )
{
family = computer.getFamily();
terminal = computer.getTerminalState();
this.displayStack = displayStack;
}
public ComputerContainerData( PacketBuffer buf )
{
family = buf.readEnum( ComputerFamily.class );
terminal = new TerminalState( buf );
displayStack = buf.readItem();
}
@Override
@ -32,6 +38,7 @@ public void toBytes( PacketBuffer buf )
{
buf.writeEnum( family );
terminal.write( buf );
buf.writeItemStack( displayStack, true );
}
public ComputerFamily family()
@ -43,4 +50,15 @@ public TerminalState terminal()
{
return terminal;
}
/**
* Get a stack associated with this menu. This may be displayed on the client.
*
* @return The stack associated with this menu.
*/
@Nonnull
public ItemStack displayStack()
{
return displayStack;
}
}

View File

@ -1,45 +0,0 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.network.server;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.inventory.container.Container;
import net.minecraft.network.PacketBuffer;
import net.minecraftforge.fml.network.NetworkEvent;
import javax.annotation.Nonnull;
public class ContinueUploadMessage extends ComputerServerMessage
{
private final boolean overwrite;
public ContinueUploadMessage( Container menu, boolean overwrite )
{
super( menu );
this.overwrite = overwrite;
}
public ContinueUploadMessage( @Nonnull PacketBuffer buf )
{
super( buf );
overwrite = buf.readBoolean();
}
@Override
public void toBytes( @Nonnull PacketBuffer buf )
{
super.toBytes( buf );
buf.writeBoolean( overwrite );
}
@Override
protected void handle( NetworkEvent.Context context, @Nonnull ComputerMenu container )
{
ServerPlayerEntity player = context.getSender();
if( player != null ) container.getInput().confirmUpload( player, overwrite );
}
}

View File

@ -168,7 +168,7 @@ public ActionResult<ItemStack> use( World world, PlayerEntity player, @Nonnull H
if( !stop )
{
boolean isTypingOnly = hand == Hand.OFF_HAND;
new ComputerContainerData( computer ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) );
new ComputerContainerData( computer, stack ).open( player, new PocketComputerMenuProvider( computer, stack, this, hand, isTypingOnly ) );
}
}
return new ActionResult<>( ActionResultType.SUCCESS, stack );

View File

@ -119,16 +119,13 @@
"gui.computercraft.upload.success": "Upload Succeeded",
"gui.computercraft.upload.success.msg": "%d files uploaded.",
"gui.computercraft.upload.failed": "Upload Failed",
"gui.computercraft.upload.failed.out_of_space": "Not enough space on the computer for these files.",
"gui.computercraft.upload.failed.computer_off": "You must turn the computer on before uploading files.",
"gui.computercraft.upload.failed.too_much": "Your files are too large to be uploaded.",
"gui.computercraft.upload.failed.name_too_long": "File names are too long to be uploaded.",
"gui.computercraft.upload.failed.too_many_files": "Cannot upload this many files.",
"gui.computercraft.upload.failed.overwrite_dir": "Cannot upload %s, as there is already a directory with the same name.",
"gui.computercraft.upload.failed.generic": "Uploading files failed (%s)",
"gui.computercraft.upload.failed.corrupted": "Files corrupted when uploading. Please try again.",
"gui.computercraft.upload.overwrite": "Files would be overwritten",
"gui.computercraft.upload.overwrite.detail": "The following files will be overwritten when uploading. Continue?%s",
"gui.computercraft.upload.overwrite_button": "Overwrite",
"gui.computercraft.upload.no_response": "Transferring Files",
"gui.computercraft.upload.no_response.msg": "Your computer has not used your transferred files. You may need to run the %s program and try again.",
"gui.computercraft.pocket_computer_overlay": "Pocket computer open. Press ESC to close."
}

View File

@ -211,6 +211,8 @@ local function tabulateCommon(bPaged, ...)
end
print()
end
local previous_colour = term.getTextColour()
for _, t in ipairs(tAll) do
if type(t) == "table" then
if #t > 0 then
@ -220,6 +222,7 @@ local function tabulateCommon(bPaged, ...)
term.setTextColor(t)
end
end
term.setTextColor(previous_colour)
end
--[[- Prints tables in a structured form.

View File

@ -0,0 +1,70 @@
-- Internal module for handling file uploads. This has NO stability guarantees,
-- and so SHOULD NOT be relyed on in user code.
local completion = require "cc.completion"
return function(files)
local overwrite = {}
for _, file in pairs(files) do
local filename = file.getName()
local path = shell.resolve(filename)
if fs.exists(path) then
if fs.isDir(path) then
return nil, filename .. " is already a directory."
end
overwrite[#overwrite + 1] = filename
end
end
if #overwrite > 0 then
table.sort(overwrite)
printError("The following files will be overwritten:")
textutils.pagedTabulate(colours.cyan, overwrite)
while true do
io.write("Overwrite? (yes/no) ")
local input = read(nil, nil, function(t)
return completion.choice(t, { "yes", "no" })
end)
if not input then return end
input = input:lower()
if input == "" or input == "yes" or input == "y" then
break
elseif input == "no" or input == "n" then
return
end
end
end
for _, file in pairs(files) do
local filename = file.getName()
print("Transferring " .. filename)
local path = shell.resolve(filename)
local handle, err = fs.open(path, "wb")
if not handle then return nil, err end
-- Write the file without loading it all into memory. This uses the same buffer size
-- as BinaryReadHandle. It would be really nice to have a way to do this without
-- multiple copies.
while true do
local chunk = file.read(8192)
if not chunk then break end
local ok, err = pcall(handle.write, chunk)
if not ok then
handle.close()
-- Probably an out-of-space issue, just bail.
if err:sub(1, 7) == "pcall: " then err = err:sub(8) end
return nil, "Failed to write file (" .. err .. "). File may be corrupted"
end
end
handle.close()
end
return true
end

View File

@ -331,9 +331,8 @@ while #tProcesses > 0 do
resizeWindows()
redrawMenu()
elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" then
-- Keyboard event
-- Passthrough to current process
elseif sEvent == "char" or sEvent == "key" or sEvent == "key_up" or sEvent == "paste" or sEvent == "terminate" or sEvent == "file_transfer" then
-- Basic input, just passthrough to current process
resumeProcess(nCurrentProcess, table.unpack(tEventData, 1, tEventData.n))
if cullProcess(nCurrentProcess) then
setMenuVisible(#tProcesses >= 2)

View File

@ -0,0 +1,24 @@
require "cc.completion"
print("Drop files to transfer them to this computer")
local files
while true do
local event, arg = os.pullEvent()
if event == "file_transfer" then
files = arg.getFiles()
break
elseif event == "key" and arg == keys.q then
return
end
end
if #files == 0 then
printError("No files to transfer")
return
end
package.path = package.path .. "/rom/modules/internal/?.lua"
local ok, err = require("cc.import")(files)
if not ok and err then printError(err) end

View File

@ -10,7 +10,6 @@
--
-- @module[module] shell
local expect = dofile("rom/modules/main/cc/expect.lua").expect
local make_package = dofile("rom/modules/main/cc/require.lua").make
local multishell = multishell
@ -35,6 +34,14 @@ local function createShellEnv(dir)
return env
end
-- Set up a dummy require based on the current shell, for loading some of our internal dependencies.
local require
do
local env = setmetatable(createShellEnv("/rom/modules/internal"), { __index = _ENV })
require = env.require
end
local expect = require("cc.expect").expect
-- Colours
local promptColour, textColour, bgColour
if term.isColour() then
@ -591,6 +598,13 @@ if #tArgs > 0 then
shell.run(...)
else
local function show_prompt()
term.setBackgroundColor(bgColour)
term.setTextColour(promptColour)
write(shell.dir() .. "> ")
term.setTextColour(textColour)
end
-- "shell"
-- Print the header
term.setBackgroundColor(bgColour)
@ -607,21 +621,49 @@ else
local tCommandHistory = {}
while not bExit do
term.redirect(parentTerm)
term.setBackgroundColor(bgColour)
term.setTextColour(promptColour)
write(shell.dir() .. "> ")
term.setTextColour(textColour)
show_prompt()
local sLine
if settings.get("shell.autocomplete") then
sLine = read(nil, tCommandHistory, shell.complete)
else
sLine = read(nil, tCommandHistory)
local complete
if settings.get("shell.autocomplete") then complete = shell.complete end
local ok, result
local co = coroutine.create(read)
assert(coroutine.resume(co, nil, tCommandHistory, complete))
while coroutine.status(co) ~= "dead" do
local event = table.pack(os.pullEvent())
if event[1] == "file_transfer" then
-- Abandon the current prompt
local _, h = term.getSize()
local _, y = term.getCursorPos()
if y == h then
term.scroll(1)
term.setCursorPos(1, y)
else
term.setCursorPos(1, y + 1)
end
term.setCursorBlink(false)
-- Run the import script with the provided files
local ok, err = require("cc.import")(event[2].getFiles())
if not ok and err then printError(err) end
-- And attempt to restore the prompt.
show_prompt()
term.setCursorBlink(true)
event = { "term_resize", n = 1 } -- Nasty hack to force read() to redraw.
end
if result == nil or event[1] == result or event[1] == "terminate" then
ok, result = coroutine.resume(co, table.unpack(event, 1, event.n))
if not ok then error(result, 0) end
end
end
if sLine:match("%S") and tCommandHistory[#tCommandHistory] ~= sLine then
table.insert(tCommandHistory, sLine)
if result:match("%S") and tCommandHistory[#tCommandHistory] ~= result then
table.insert(tCommandHistory, result)
end
shell.run(sLine)
shell.run(result)
end
end

View File

@ -6,13 +6,12 @@
package dan200.computercraft.support;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.contains;
public class CustomMatchers
{
/**
@ -27,6 +26,7 @@ public class CustomMatchers
*/
public static <T> Matcher<Iterable<? extends T>> containsWith( List<T> items, Function<T, Matcher<? super T>> matcher )
{
return contains( items.stream().map( matcher ).collect( Collectors.toList() ) );
// The explicit type argument should be redundant, but it appears some Java compilers require it.
return Matchers.<T>contains( items.stream().map( matcher ).collect( Collectors.toList() ) );
}
}

View File

@ -1,3 +1,5 @@
local with_window = require "test_helpers".with_window
describe("The shell", function()
describe("require", function()
it("validates arguments", function()
@ -101,4 +103,48 @@ describe("The shell", function()
expect.error(shell.switchTab, nil):eq("bad argument #1 (expected number, got nil)")
end)
end)
describe("file uploads", function()
local function create_file(name, contents)
local did_read = false
return {
getName = function() return name end,
read = function()
if did_read then return end
did_read = true
return contents
end,
close = function() end,
}
end
local function create_files(files) return { getFiles = function() return files end } end
it("suspends the read prompt", function()
fs.delete("tmp.txt")
local win = with_window(32, 5, function()
local queue = {
{ "shell" },
{ "paste", "xyz" },
{ "file_transfer", create_files { create_file("transfer.txt", "empty file") } },
}
local co = coroutine.create(shell.run)
for _, event in pairs(queue) do assert(coroutine.resume(co, table.unpack(event))) end
end)
expect(win.getCursorBlink()):eq(true)
local lines = {}
for i = 1, 5 do lines[i] = win.getLine(i):gsub(" +$", "") end
expect(lines):same {
"CraftOS 1.8",
"> xyz",
"Transferring transfer.txt",
"> xyz",
"",
}
expect({ win.getCursorPos() }):same { 6, 4 }
end)
end)
end)